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

 

posted @   左扬  阅读(158)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
levels of contents
点击右上角即可分享
微信分享提示