Go语言的goroutine让并发编程变得简单高效,但多个goroutine同时操作共享资源时,很容易出现数据竞争问题,这会引发难以排查的程序异常。理解数据竞争的本质,掌握对应的并发同步机制,是写出正确并发程序的核心。

什么是数据竞争
数据竞争指的是两个或两个以上的goroutine同时访问同一个内存地址,并且至少有一个访问是写操作,同时这些访问之间没有同步机制做协调。这种情况下,程序的执行结果会因为goroutine的调度顺序不同而产生差异,属于未定义行为。
比如下面这段简单的代码,两个goroutine同时对一个全局变量做累加操作,就存在数据竞争:
package main
import (
"fmt"
"time"
)
var count int
func add() {
for i := 0; i < 1000; i++ {
count++ // 多个goroutine同时写count,存在数据竞争
}
}
func main() {
go add()
go add()
time.Sleep(time.Second)
fmt.Println("最终count值:", count)
}
多次运行上面的程序,得到的结果可能不是预期的2000,就是因为数据竞争导致部分累加操作被覆盖。
Go语言内置的并发同步机制
互斥锁(sync.Mutex)
互斥锁是最基础的同步工具,它保证同一时间只有一个goroutine能持有锁,从而访问被保护的共享资源。使用sync.Mutex可以解决上面的数据竞争问题:
package main
import (
"fmt"
"sync"
"time"
)
var (
count int
mu sync.Mutex // 定义互斥锁
)
func add() {
for i := 0; i < 1000; i++ {
mu.Lock() // 获取锁,其他goroutine无法进入临界区
count++
mu.Unlock() // 释放锁,允许其他goroutine获取
}
}
func main() {
go add()
go add()
time.Sleep(time.Second)
fmt.Println("最终count值:", count)
}
互斥锁的使用要注意成对出现,避免死锁。如果已经持有锁的goroutine再次尝试获取锁,就会陷入阻塞,导致程序卡死。
读写锁(sync.RWMutex)
如果共享资源的读操作远多于写操作,使用互斥锁会导致所有读操作也串行执行,效率较低。这时候可以使用读写锁sync.RWMutex,它允许多个goroutine同时获取读锁,但是写锁是互斥的,读锁和写锁之间也互斥。
package main
import (
"fmt"
"sync"
"time"
)
var (
data map[string]int
rwMu sync.RWMutex
)
func init() {
data = make(map[string]int)
}
// 写操作,使用写锁
func writeData(key string, value int) {
rwMu.Lock()
defer rwMu.Unlock()
data[key] = value
fmt.Printf("写入数据: %s=%dn", key, value)
}
// 读操作,使用读锁
func readData(key string) int {
rwMu.RLock()
defer rwMu.RUnlock()
return data[key]
}
func main() {
go writeData("a", 1)
go readData("a")
time.Sleep(time.Second)
}
通道(channel)
Go语言的设计哲学是不要通过共享内存来通信,而是通过通信来共享内存。通道是goroutine之间传递数据的管道,本身就具备同步特性,使用通道可以天然避免数据竞争。
上面的累加问题用通道实现的版本如下:
package main
import (
"fmt"
)
func main() {
count := 0
ch := make(chan int, 1000) // 缓冲通道,用来传递累加任务
// 启动两个goroutine发送累加任务
for i := 0; i < 2; i++ {
go func() {
for j := 0; j < 1000; j++ {
ch <- 1 // 发送累加值1
}
}()
}
// 主goroutine接收通道数据做累加
for i := 0; i < 2000; i++ {
count += <-ch
}
close(ch)
fmt.Println("最终count值:", count)
}
其他同步工具
除了上述工具,sync包还提供了WaitGroup用来等待一组goroutine执行完成,Once用来保证某个操作只执行一次,Cond用来实现条件等待,这些工具都可以根据场景配合使用。
如何检测数据竞争
Go语言内置了数据竞争检测工具,只需要在编译或者运行程序时加上-race参数即可:
go run -race main.go
如果程序存在数据竞争,运行时会输出详细的竞争发生的位置、涉及的goroutine等信息,帮助开发者快速定位问题。
避免数据竞争的实践建议
- 尽量减少共享资源的使用,优先通过通道在goroutine之间传递数据,而不是共享变量。
- 如果必须使用共享资源,要明确临界区范围,用合适的同步工具保护,锁的粒度不要过大也不要过小。
- 开发阶段始终用
-race参数检测程序,提前发现潜在的数据竞争问题。 - 避免在持有锁的情况下调用外部未知的函数,防止死锁或者锁持有时间过长。