1. Golang 中的“数据竞争”
我在上个月发过一篇《跟着 GPT-4 从0到1学习 Golang 并发机制(三)》,文中有一节专门介绍了“Race Detector 检测数据竞争”。
数据竞争发生在当两个或更多的 goroutine 并发访问同一块内存区域,且至少有一个访问是写入操作时。比如这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
)
var counter int
func increment() {
counter++
}
func main() {
go increment()
go increment()
fmt.Println(counter)
}
|
在这个程序中,两个 goroutine 都在尝试增加 counter 的值。这会导致数据竞争,因为 counter++ 不是一个原子操作。
如果你使用 Race Detector 来运行这个程序(go run -race main.go),它会报告数据竞争,并给出详细的报告,包括数据竞争发生的位置和涉及的 goroutine。
2. GoPool 中的数据竞争问题
又聊到 GoPool 了。没错,还是那个宣称自己性能全网第一的 Golang Worker Pool 实现。一个普通程序员拿着 GPT-4 只花了3天就肝出来的一个高性能 Worker 池。
关于 GoPool 的介绍可以跳转这篇文章:
昨天就有人提出 GoPool 中可能存在 goroutine 泄露问题:
那位社区贡献者还提了一个 PR,然后为此我专门写了一篇文章来详细介绍这个 issue 的“前世今生”:
今天又有人提出了 GoPool 的测试用例中存在“数据竞争”问题:
在这个 issue 中贡献者贴了这样一段日志:
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
$ go test -v -race ./...
=== RUN TestGoPoolWithMutex
--- PASS: TestGoPoolWithMutex (0.11s)
=== RUN TestGoPoolWithSpinLock
--- PASS: TestGoPoolWithSpinLock (0.11s)
=== RUN TestGoPoolWithError
--- PASS: TestGoPoolWithError (0.11s)
=== RUN TestGoPoolWithResult
--- PASS: TestGoPoolWithResult (0.11s)
=== RUN TestGoPoolWithRetry
==================
WARNING: DATA RACE
Read at 0x00c00001c258 by goroutine 423:
github.com/devchat-ai/gopool.TestGoPoolWithRetry()
/workspaces/gopool/gopool_test.go:147 +0x284
testing.tRunner()
/usr/local/go/src/testing/testing.go:1576 +0x216
testing.(*T).Run.func1()
/usr/local/go/src/testing/testing.go:1629 +0x47
Previous write at 0x00c00001c258 by goroutine 523:
github.com/devchat-ai/gopool.TestGoPoolWithRetry.func1()
/workspaces/gopool/gopool_test.go:138 +0x64
github.com/devchat-ai/gopool.(*worker).executeTaskWithoutTimeout()
/workspaces/gopool/worker.go:78 +0xd1
github.com/devchat-ai/gopool.(*worker).executeTask()
/workspaces/gopool/worker.go:41 +0xc7
github.com/devchat-ai/gopool.(*worker).start.func1()
/workspaces/gopool/worker.go:26 +0xaa
Goroutine 423 (running) created at:
testing.(*T).Run()
/usr/local/go/src/testing/testing.go:1629 +0x805
testing.runTests.func1()
/usr/local/go/src/testing/testing.go:2036 +0x8d
testing.tRunner()
/usr/local/go/src/testing/testing.go:1576 +0x216
testing.runTests()
/usr/local/go/src/testing/testing.go:2034 +0x87c
testing.(*M).Run()
/usr/local/go/src/testing/testing.go:1906 +0xb44
main.main()
_testmain.go:61 +0x2e9
Goroutine 523 (running) created at:
github.com/devchat-ai/gopool.(*worker).start()
/workspaces/gopool/worker.go:23 +0xf7
github.com/devchat-ai/gopool.NewGoPool()
/workspaces/gopool/gopool.go:75 +0x54f
github.com/devchat-ai/gopool.TestGoPoolWithRetry()
/workspaces/gopool/gopool_test.go:134 +0xfb
testing.tRunner()
/usr/local/go/src/testing/testing.go:1576 +0x216
testing.(*T).Run.func1()
/usr/local/go/src/testing/testing.go:1629 +0x47
==================
testing.go:1446: race detected during execution of test
--- FAIL: TestGoPoolWithRetry (0.10s)
=== NAME
testing.go:1446: race detected during execution of test
FAIL
FAIL github.com/devchat-ai/gopool 0.558s
FAIL
|
啥?又有 issue?那我得立刻马上现在就把它“秒”掉!
3. 让 GPT-4 来修复数据竞争问题
问题很明确了,最大的问题是“日志有点长”,次要问题是“我有点懒”。没错,我是闭口不提如何从日志中寻找“哪里少加了锁导致数据竞争”。
我决定将这个问题抛给 GPT-4,让 GPT-4 直接定位问题,修复问题,再帮我写好 commit message,一条龙服务。
下文使用 DevChat 和 GPT-4 交互。如果你对 DevChat 感兴趣,可以跳转阅读这篇文章:
3.1 和 GPT-4 的第一轮沟通
- The user is running tests on a Go project and encountered a data race error.
- The error is occurring in the
TestGoPoolWithRetry
function in the gopool_test.go
file.
- The data race is happening between two goroutines, one reading and the other writing to the same memory address.
- The reading is happening in the
TestGoPoolWithRetry
function, and the writing is happening in the TestGoPoolWithRetry.func1
function, which is called by the executeTaskWithoutTimeout
function in the worker.go
file.
To fix this issue, we need to ensure that the taskRunCount
variable is accessed in a thread-safe manner. We can achieve this by using a mutex to lock the variable when it’s being read or written. Here is the modified TestGoPoolWithRetry
function:
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
|
func TestGoPoolWithRetry(t *testing.T) {
var retryCount = 3
var taskError = errors.New("task error")
var taskRunCount = 0
var mu sync.Mutex
pool := NewGoPool(100, WithRetryCount(retryCount))
defer pool.Release()
pool.AddTask(func() (interface{}, error) {
mu.Lock()
taskRunCount++
mu.Unlock()
if taskRunCount <= retryCount {
return nil, taskError
}
return nil, nil
})
pool.Wait()
mu.Lock()
if taskRunCount != retryCount+1 {
t.Errorf("Expected task to run %v times, but it ran %v times", retryCount+1, taskRunCount)
}
mu.Unlock()
}
|
In this modification, we introduced a mutex mu
and used it to lock taskRunCount
when it’s being incremented and read. This ensures that only one goroutine can access taskRunCount
at a time, preventing the data race.
因为这轮聊天我发送的内容都是英文的,所以 GPT-4 也就“傲娇”地全英文响应了。扫一眼这段代码不难发现 GPT-4 通过在 taskRunCount 操作前后加互斥锁来解决“数据竞争问题。
3.2 和 GPT-4 的第二轮沟通
我觉得一个小小的 taskRunCount 数据保护,用原子操作似乎更简洁;于是我又对 DevChat 说:
是不是改成原子操作会更简洁?
是的,使用原子操作可以更简洁地解决这个问题,而且性能也会更好。我们可以使用 sync/atomic
包中的 AddInt32
和 LoadInt32
函数来实现。这里是修改后的 TestGoPoolWithRetry
函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import "sync/atomic"
func TestGoPoolWithRetry(t *testing.T) {
var retryCount = int32(3)
var taskError = errors.New("task error")
var taskRunCount int32 = 0
pool := NewGoPool(100, WithRetryCount(int(retryCount)))
defer pool.Release()
pool.AddTask(func() (interface{}, error) {
atomic.AddInt32(&taskRunCount, 1)
if taskRunCount <= retryCount {
return nil, taskError
}
return nil, nil
})
pool.Wait()
if atomic.LoadInt32(&taskRunCount) != retryCount+1 {
t.Errorf("Expected task to run %v times, but it ran %v times", retryCount+1, taskRunCount)
}
}
|
在这个修改中,我们使用 atomic.AddInt32
来原子地增加 taskRunCount
,并使用 atomic.LoadInt32
来原子地读取 taskRunCount
。这样可以确保在并发环境下对 taskRunCount
的访问是线程安全的。
3.3 提交代码
这个修改不算复杂,GPT-4 也是一遍梭哈,目测没问题(当然手测也没有问题)。于是我继续让 GPT-4 写了一个 commit message:
就这样,我提交了这个 PR:
点开看看呗,这个 PR 里的“英文描述”还是挺地道的呢!是不是给人一种“我英文还不错”的错觉?哇咔咔,GPT 在手,你都分辨不了我的母语是汉语还是英语。
4. 总结
按理说,这里需要有一个总结。
但是,我不喜欢按理说。
拜拜,我下班了。
明天见。
bye……