Go从入门到精通——同步——保证并发环境下数据访问的准确性(竞态检测、互斥锁、读写互斥锁)
同步——保证并发环境下数据访问的准确性(竞态检测、互斥锁、读写互斥锁)
Go 程序可以使用通道进行多个 goroutine 间的数据交换,但这仅仅是数据同步中的一种方法。通道内部的实现依然使用了各种锁,因此优雅代码的代价是性能。在某些轻量级的场合,原子访问(atomic包)、互斥锁(sync.Mutex)以及等待组(sync.WaitGroup)能最大程度满足需求。
一、竞态检测——检测代码在并发环境下可能出现的问题
当多线程并发运行的程序竞争访问和修改同一块资源时,会发生竞态问题。
下面的代码中有一个 ID生成器,每次调用生成器将会生成一个不会重复的顺序序号,使用 10个并发生成序号,观察 10个并发后的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package main import ( "fmt" "sync/atomic" ) var ( //序号 seq int64 ) //序号生成器 func GenID() int64 { //使用原子操作函数 atomic.AddInt64() 对 seq加 1操作。 //这里没有使用 atomic.AddInt64()的返回值作为 GenID()函数的返回值, //用来引出一个竞态问题。 atomic.AddInt64(&seq, 1) return seq } func main() { //循环10次,生成10个 goroutine 调用 GenID() 函数 for i := 0; i <= 10; i++ { go GenID() } //单独调用一次 GEnID() 函数 fmt.Println(GenID()) } |
在运行程序时,为运行参数加入 "-race" 参数,开启运行时(runtime)对竞态问题的分析,命令如下:
1 | PS D:\ go -testfiles> go run -race .\racedetect. go |
代码运行发生宕机,输出信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | PS D:\ go -testfiles> go run -race .\racedetect. go ================== WARNING: DATA RACE Write at 0x0000011fd4f0 by goroutine 8: sync/atomic.AddInt64() C:/Program Files/Go/src/runtime/race_amd64.s:287 +0xb sync/atomic.AddInt64() <autogenerated>:1 +0x1b main.main.func1() D:/ go -testfiles/racedetect. go :27 +0x2b Previous read at 0x0000011fd4f0 by goroutine 7: main.GenID() D:/ go -testfiles/racedetect. go :20 +0x3a main.main.func1() PS D:\ go -testfiles> go run -race .\racedetect.gocls go : go .mod file not found in current directory or any parent directory; see 'go help modules' PS D:\ go -testfiles> go run -race .\racedetect. go ================== WARNING: DATA RACE Write at 0x00000023d4f0 by goroutine 8: sync/atomic.AddInt64() C:/Program Files/Go/src/runtime/race_amd64.s:287 +0xb sync/atomic.AddInt64() <autogenerated>:1 +0x1b main.main.func1() D:/ go -testfiles/racedetect. go :27 +0x2b Previous read at 0x00000023d4f0 by goroutine 7: main.GenID() D:/ go -testfiles/racedetect. go :20 +0x3a main.main.func1() D:/ go -testfiles/racedetect. go :27 +0x2b Goroutine 8 (running) created at: main.main() D:/ go -testfiles/racedetect. go :27 +0x39 Goroutine 7 (finished) created at: main.main() D:/ go -testfiles/racedetect. go :27 +0x39 ================== 11 Found 1 data race(s) exit status 66 PS D:\ go -testfiles> |
根据报错信息,在 20行 发现有竞态问题,
1 2 3 4 5 | Previous read at 0x00000023d4f0 by goroutine 7: main.GenID() D:/ go -testfiles/racedetect. go :20 +0x3a main.main.func1() D:/ go -testfiles/racedetect. go :27 +0x2b |
我们修改下:
1 2 3 4 | //序号生成器 func GenID() int64 { return atomic.AddInt64(&seq, 1) } |
再次运行:
1 2 3 | PS D:\ go -testfiles> go run -race .\racedetect. go 11 PS D:\ go -testfiles> |
没有发生竞态问题了,程序运行正常。
这个例子也可以用下面的互斥锁(sync.Mutex)解决,但是对性能消耗较大,在这种情况下,推荐使用原子操作(atomic)进行变量操作。
二、互斥锁(sync.Mutex)——保证同时只有一个 goroutine 可以访问共享资源
互斥锁是一种常用的控制共享资源访问的方法。在 Go 程序中的使用非常简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package main import ( "fmt" "sync" ) var ( //逻辑中使用的某个变量 count int //与变量对应的使用互斥锁,这里将互斥锁的变量命名为 变量名+Guard,以表示保护这个变量 countGuard sync.Mutex ) func GetCount() int { //锁定 countGuard.Lock() //在函数退出时接触锁定 defer countGuard.Unlock() return count } func SetCount(c int) { countGuard.Lock() count = c countGuard.Unlock() } func main() { //可以进行并发安全的设置 SetCount(1) //可以进行并发安全的获取 fmt.Println(GetCount()) } |
三、读写互斥锁(sync.RWMutex)——在读比写多的环境下比互斥锁更高效
在读多写少的环境中,可以优先使用读写互斥锁,sync 包中的 RWMutex 提供了读写互斥锁的封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package main import ( "fmt" "sync" ) var ( //逻辑中使用的某个变量 count int //与变量对应的使用互斥锁,这里将互斥锁的变量命名为 变量名+Guard,以表示保护这个变量 countGuard sync.RWMutex ) func GetCount() int { //锁定 countGuard.RLock() //在函数退出时接触锁定 defer countGuard.RUnlock() return count } func SetCount(c int) { countGuard.RLock() count = c countGuard.RUnlock() } func main() { //可以进行并发安全的设置 SetCount(1) //可以进行并发安全的获取 fmt.Println(GetCount()) } |
四、等待组(sync.WaitGroup)——保证在并发环境中完成指定数量的任务
除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步。
等待组有下面几个方法可用,如下表:
方法名 | 功能 |
(wg *WaitGroup)Add(delta int) | 等待组的计数器+1 |
(wg *WaitGroup)Done() | 等待组的计数器-1 |
(wg *WaitGroup)Wait() | 当等待组计数器不等于0时阻塞直到变0 |
等待组内部拥有一个计数器,计数器的值可以通过方法调用实现计数器的增加和减少。
当我们添加了 N 个并发任务进行工作时,就将等待组的计数器值增加 N。每个任务完成时,这个值减 1。同时,在另外一个 goroutine 中等待这个等待组的计数器值为 0 时,表示所有任务已经完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | package main import ( "fmt" "net/http" "sync" ) func main() { //声明一个等待组 var wg sync.WaitGroup //准备一系列的网站地址 var urls = []string{ "https://www.baidu.com" , "https://www.jd.com" , "https://www.taobao.com" , "https://www.google.com" , } //遍历这些地址 for _, url := range urls { //每一个任务开始时,请等待组增加 1 wg.Add(1) //开启一个并发 go func (url string) { //使用 defer,表示函数完成时将等待组减少 1 defer wg.Done() //使用 HTTP 访问提供的地址 _, err := http.Get(url) //访问完成后,打印地址和可能发生的错误 fmt.Println(url, err) //通过参数传递 url 地址 }(url) } //等待所有的任务完成 wg.Wait() fmt.Println( "over" ) } |
程序运行后:
1 2 3 4 5 6 7 8 9 10 11 | Starting: D:\ go -testfiles\bin\dlv.exe dap --check- go -version=false --listen=127.0.0.1:52039 from d:\ go -testfiles DAP server listening at: 127.0.0.1:52039 Type 'dlv help' for list of commands. https: //www.baidu.com <nil> https: //www.jd.com <nil> https: //www.taobao.com <nil> https: //www.google.com Get "https://www.google.com": dial tcp 162.125.32.6:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond. over Process 20104 has exited with status 0 Detaching dlv dap (5628) exited with code: 0 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南