概要
本文旨在快速了解golang的并发编程代码实际编写, 至于理论方面不会讲解太多
我将以题目的形式来进行讲解
带有goroutine waitGroup channel select mutex RWMutex
以及生产-消费者模型的练习
goroutine
关于Goroutine的基本使用
启动多个 Goroutine 打印数字
编写一个程序,启动 5 个 Goroutine,每个 Goroutine 分别打印一个从 1 到 5 的数字。要求每个 Goroutine 独立运行。
用法提示:
Goroutine 是 Go 并发的基本单元,使用 go 关键字启动。它们独立执行,不会阻塞主线程,但需要注意主 Goroutine 的生命周期。Goroutines 不会自动同步,需要配合其它同步手段来控制程序的退出或结果。
答案
package main
import "time"
func main() {
go func() {
println(1)
}()
go func() {
println(2)
}()
go func() {
println(3)
}()
go func() {
println(4)
}()
go func() {
println(5)
}()
time.Sleep(2000 * time.Millisecond)
}
WaitGroup
使用 sync.WaitGroup 等待 Goroutines 完成
在这个题目中,你需要修改上一个程序,使用 sync.WaitGroup 来替代 time.Sleep(),确保主 Goroutine 等待所有 Goroutines 完成后才退出。
用法提示:
sync.WaitGroup 是 Go 中用于等待一组 Goroutines 完成的同步原语。可以通过 Add() 方法增加等待的 Goroutine 数量,通过 Done() 方法减少计数,最后通过 Wait() 方法阻塞主 Goroutine,直到所有 Goroutine 执行完毕。
var wg sync.WaitGroup
wg.Add(1) // 增加 Goroutine 计数
go func() {
defer wg.Done() // Goroutine 完成时调用 Done 减少计数
// 执行任务
}()
wg.Wait() // 阻塞主 Goroutine,直到计数归零
答案
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
println(1)
}()
wg.Add(1)
go func() {
defer wg.Done()
println(2)
}()
wg.Add(1)
go func() {
defer wg.Done()
println(3)
}()
wg.Add(1)
go func() {
defer wg.Done()
println(4)
}()
wg.Add(1)
go func() {
defer wg.Done()
println(5)
}()
wg.Wait()
}
channel
使用 Channel 实现 Goroutines 间的通信
编写一个程序,启动两个 Goroutines,一个负责生成 1 到 10 的数字,另一个 Goroutine 负责接收这些数字并将它们打印出来。要求这两个 Goroutines 通过 Channel 通信。
用法提示:
Channel 是 Go 并发编程中用于 Goroutines 之间通信的机制。通过 Channel,可以安全地在多个 Goroutine 之间传递数据。使用 make(chan T) 创建一个类型为 T 的 Channel。通过 <- 操作符发送和接收数据。
ch := make(chan int) // 创建一个整型 Channel
go func() {
ch <- 10 // 发送数据到 Channel
}()
value := <-ch // 从 Channel 接收数据
注意这里需要close关闭channel告知接收方不会再有数据传入了, 不然会有死锁风险
答案
package main
import "sync"
func main() {
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 10; i++ {
ch <- i
}
close(ch) // 关闭channel, 通知接收方没有更多的数据了
}()
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 10; i++ {
println(<-ch)
}
}()
wg.Wait()
}
bufferedChannel
使用 Buffered Channel 实现并发任务结果收集
编写一个程序,启动 3 个 Goroutines,每个 Goroutine 执行一个计算任务,将它们的计算结果发送到一个 缓冲区大小为 3 的 Channel 中。主 Goroutine 需要从 Channel 中读取所有结果并打印出来。每个 Goroutine 的任务如下:
第一个 Goroutine 计算从 1 到 5 的和。
第二个 Goroutine 计算从 6 到 10 的和。
第三个 Goroutine 计算从 11 到 15 的和。
主 Goroutine 读取 Channel 中的结果并打印每个任务的计算结果。
用法提示
使用 Buffered Channel 来确保发送数据不会阻塞 Goroutines。
make(chan int, 3):创建一个带有 3 个缓冲区的 Channel。
各个 Goroutine 完成计算后,将结果发送到 Channel 中。
ch := make(chan int, 3) // 创建一个缓冲区大小为 3 的 Channel
ch <- 1 // 向缓冲区发送数据,未被消费时不会阻塞,直到缓冲区满
ch <- 2
ch <- 3
value := <-ch // 消费数据,缓冲区释放
错误示范
package main
import (
"fmt"
"sync"
)
// func main() {
func A() {
ch := make(chan int, 3)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
var sum int
for i := 1; i <= 5; i++ {
sum += i
}
ch <- sum
}()
wg.Add(1)
go func() {
defer wg.Done()
var sum int
for i := 6; i <= 10; i++ {
sum += i
}
ch <- sum
}()
wg.Add(1)
go func() {
defer wg.Done()
var sum int
for i := 11; i <= 15; i++ {
sum += i
}
ch <- sum
}()
wg.Add(1)
go func() {
defer wg.Done()
var sum int
for i := 1; i <= 3; i++ {
sum += <-ch
}
fmt.Println(sum)
}()
wg.Wait()
/*
在最后一个 Goroutine 中,尝试计算所有 Goroutines 传回的和,
但这个 Goroutine 和前面计算任务的 Goroutines 是同时执行的。
如果主 Goroutine提前开始读取 Channel 数据,
其他 Goroutines 还没有来得及发送数据,这就可能导致程序行为不符合预期,
甚至可能出现死锁
*/
}
答案
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 3)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
var sum int
for i := 1; i <= 5; i++ {
sum += i
}
ch <- sum
}()
wg.Add(1)
go func() {
defer wg.Done()
var sum int
for i := 6; i <= 10; i++ {
sum += i
}
ch <- sum
}()
wg.Add(1)
go func() {
defer wg.Done()
var sum int
for i := 11; i <= 15; i++ {
sum += i
}
ch <- sum
}()
go func() {
wg.Wait()
close(ch)
}()
var ans int
for sum := range ch {
ans += sum
}
fmt.Println(ans)
}
当主程序执行 for sum := range ch { ans += sum } 时,ch 最终一定会被关闭。
具体来说,主 Goroutine 会在执行 wg.Wait() 之后通过 close(ch) 关闭 Channel。因为 close(ch) 是在一个 Goroutine 中执行的,而 wg.Wait() 会阻塞直到所有的计算 Goroutines 完成,因此只有当所有 Goroutines 都执行完毕后,才会执行 close(ch),然后主程序才开始遍历 Channel。
生产者-消费者模式
编写一个程序,实现生产者-消费者模式。创建一个缓冲区大小为 5 的 Channel,启动一个生产者 Goroutine 和一个消费者 Goroutine。
生产者:
生成 20 个整数(从 1 到 20),并将它们发送到 Channel。
每次发送后暂停一小段时间(可以使用 time.Sleep),模拟生产的延迟。
消费者:
从 Channel 中接收数据,并打印接收到的整数。
每次接收后同样暂停一小段时间,模拟消费的延迟。
在生产者完成发送后,关闭 Channel,通知消费者没有更多数据会发送。
用法提示
使用 Buffered Channel 来避免生产者和消费者之间的阻塞。
生产者可以在 Channel 未满时继续发送数据,消费者可以在 Channel 非空时继续接收数据。
答案
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan int, 5)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 20; i++ {
ch <- i
time.Sleep(100 * time.Millisecond)
}
close(ch)
}()
wg.Add(1)
go func() {
defer wg.Done()
for {
v, ok := <-ch
time.Sleep(100 * time.Millisecond)
if !ok {
break
}
fmt.Println("正在消费信息", v)
}
}()
// 还可以这样写消费者, 当ch还没有被close的时候, 消费者如果消费完了全部数据会阻塞等待ch被关闭
// 这个for的退出取决于 数据是否被读取完毕&&管道是否被close
//wg.Add(1)
//go func() {
// defer wg.Done()
// for v := range ch { // 使用 range 读取 Channel 数据
// fmt.Println("正在消费信息", v)
// time.Sleep(100 * time.Millisecond) // 模拟消费延迟
// }
//}()
wg.Wait()
}
select
编写一个程序,其中包含两个生产者和两个消费者,使用 select 语句来处理多个 Channel 的读取和超时情况。
具体要求:
创建 Channel:
创建一个带缓冲的整型 Channel,容量为 5。
生产者:
启动两个生产者 Goroutines。
每个生产者每隔 1 秒向 Channel 中发送一个从 1 到 20 的整数(生产者可以选择不同的数字)。
当每个生产者发送完各自的 10 个数字后,关闭 Channel。
消费者:
启动两个消费者 Goroutines。
每个消费者从 Channel 中读取数据并打印出来。
使用 select 语句来处理 Channel 的接收操作,同时设置一个 5 秒的超时,如果在这段时间内没有接收到数据,消费者应该打印 “超时,未收到数据” 并结束。
结束条件:
每个消费者应在成功消费 10 个数字后停止运行。
确保在关闭 Channel 之前,消费者能够正常退出并不发生阻塞。
用法提示
select 语句用于等待多个 Channel 操作,可以用来处理发送和接收数据的操作。
使用 time.After 来设置超时,这样可以在等待 Channel 数据的同时处理超时事件。
答案
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
ch := make(chan int, 5)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 10; i++ {
ch <- rand.Intn(3) + 1
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for i := 1; i <= 10; i++ {
ch <- rand.Intn(3) + 1
time.Sleep(10 * time.Second) // 模拟超时
}
}()
go func() {
wg.Wait()
close(ch)
}()
for {
select {
case v, ok := <-ch:
if !ok {
return
}
fmt.Println("消费者处理数据", v)
case <-time.After(5 * time.Second):
fmt.Println("5秒内没有收到数据")
return
}
}
}
Mutex
编写一个程序,启动 500 个 Goroutines,每个 Goroutine 都要对同一个全局计数器 counter 进行 100 次递增操作。由于多个 Goroutine 会同时访问 counter,你需要使用 sync.Mutex 来确保并发安全。
用法提示
sync.Mutex 用于保护共享资源。
使用 mutex.Lock() 锁定共享资源,确保只有一个 Goroutine 能够访问该资源。
使用 mutex.Unlock() 解锁,允许其他 Goroutine 访问共享资源。
答案
package main
import (
"fmt"
"sync"
)
func main() {
// 如果不加锁 结果会是怎么样的
var mutex sync.Mutex
var count int
var wg sync.WaitGroup
for i := 1; i <= 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 1; j <= 100; j++ {
mutex.Lock()
// 当此处不加锁, 多个goroutine并发执行count++
// A协程读取count为15531 A协程计算+1结果 A程序把15532值赋给count
// -----------------------------------------------------------------
// B协程读取count为15531 B协程计算+1结果 B程序把15532值赋给count
// B协程读取的时候A协程还未把修改结果赋值, 导致了B协程的修改无效
count++
mutex.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}
RWMytex
使用 sync.RWMutex 实现读写锁
编写一个程序,模拟多个 Goroutines 对共享变量的读写操作。要求:
启动 5 个 Goroutines,分别执行读操作,每次读取需要通过 sync.RWMutex 确保安全,且多个读操作可以并发执行。
启动 2 个 Goroutines,分别执行写操作,每次写操作需要通过 sync.RWMutex 进行独占锁控制,确保写操作期间其他 Goroutines 无法读或写。
用法提示
rwMutex.RLock() 用于加读锁,多个 Goroutine 可以同时获取读锁。
rwMutex.RUnlock() 用于释放读锁。
rwMutex.Lock() 用于加写锁,写操作期间其他 Goroutine 无法进行读或写。
rwMutex.Unlock() 用于释放写锁。
答案
package main
import (
"fmt"
"sync"
)
type node struct {
mutex sync.RWMutex
count int
}
func main() {
var c node
var wg sync.WaitGroup
for i := 1; i <= 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.mutex.Lock()
defer c.mutex.Unlock()
c.count++
}()
}
wg.Wait()
for i := 1; i <= 200; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.mutex.RLock()
defer c.mutex.RUnlock()
fmt.Println(c.count)
}()
}
wg.Wait()
}