Go从入门到精通——同步——保证并发环境下数据访问的准确性(竞态检测、互斥锁、读写互斥锁)
同步——保证并发环境下数据访问的准确性(竞态检测、互斥锁、读写互斥锁)
Go 程序可以使用通道进行多个 goroutine 间的数据交换,但这仅仅是数据同步中的一种方法。通道内部的实现依然使用了各种锁,因此优雅代码的代价是性能。在某些轻量级的场合,原子访问(atomic包)、互斥锁(sync.Mutex)以及等待组(sync.WaitGroup)能最大程度满足需求。
一、竞态检测——检测代码在并发环境下可能出现的问题
当多线程并发运行的程序竞争访问和修改同一块资源时,会发生竞态问题。
下面的代码中有一个 ID生成器,每次调用生成器将会生成一个不会重复的顺序序号,使用 10个并发生成序号,观察 10个并发后的结果。
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)对竞态问题的分析,命令如下:
PS D:\go-testfiles> go run -race .\racedetect.go
代码运行发生宕机,输出信息如下:
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行 发现有竞态问题,
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
我们修改下:
//序号生成器
func GenID() int64 {
return atomic.AddInt64(&seq, 1)
}
再次运行:
PS D:\go-testfiles> go run -race .\racedetect.go 11 PS D:\go-testfiles>
没有发生竞态问题了,程序运行正常。
这个例子也可以用下面的互斥锁(sync.Mutex)解决,但是对性能消耗较大,在这种情况下,推荐使用原子操作(atomic)进行变量操作。
二、互斥锁(sync.Mutex)——保证同时只有一个 goroutine 可以访问共享资源
互斥锁是一种常用的控制共享资源访问的方法。在 Go 程序中的使用非常简单。
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 提供了读写互斥锁的封装。
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 时,表示所有任务已经完成。
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")
}
程序运行后:
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

浙公网安备 33010602011771号