Go并发编程避坑指南:从数据竞争到同步原语的解决方案

本文涉及的产品
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时计算 Flink 版,1000CU*H 3个月
实时数仓Hologres,5000CU*H 100GB 3个月
简介: 在高并发场景下,如钱包转账,数据一致性至关重要。本文通过实例演示了 Go 中如何利用 `sync.Mutex` 和 `sync.RWMutex` 解决数据竞争问题,帮助开发者掌握并发编程中的关键技能。

在日常开发中,我们经常会遇到高并发的业务场景,比如钱包系统的转账。如何保证并发情况下的数据一致性,是 Go 工程师必须掌握的技能之一。今天我用一个简单的钱包转账例子,带大家看看 Go 中数据竞争是怎么发生的,以及如何用 sync.Mutexsync.RWMutex 来解决。


1. 从问题开始:并发转账的数据错乱

假设我们实现了一个简单的钱包结构体 Wallet,并提供了转账方法:

代码语言:go


AI代码解释

type Wallet struct {
    Balance int
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
    if w.Balance >= amount {
        w.Balance -= amount
        target.Balance += amount
    }
}

main 函数里,我们让两个用户账户各自初始余额 1000,然后模拟 1000 个并发协程,每次从 userA 转账 1 元给 userB

代码语言:go


AI代码解释

func main() {
    userA := &Wallet{Balance: 1000}
    userB := &Wallet{Balance: 1000}
    var wg sync.WaitGroup
    wg.Add(1000)
    for i := 0; i < 1000; i++ {
        go func() {
            defer wg.Done()
            userA.Transfer(1, userB)
        }()
    }
    wg.Wait()
    fmt.Printf("UserA余额: %d\n", userA.Balance)
  fmt.Printf("UserB余额: %d\n", userB.Balance)
}

预期结果:

  • A 转出 1000 元后余额为 0
  • B 收入 1000 元后余额为 2000

但实际运行多次后,输出往往并不一致,比如:

代码语言:bash


AI代码解释

UserA余额: 312
UserB余额: 1688

为什么会这样?


2. 问题的根源:数据竞争

原因在于:

  • 多个 goroutine 同时在修改 userA.BalanceuserB.Balance
  • w.Balance -= amounttarget.Balance += amount 并不是原子操作
  • 导致读写交叉时数据覆盖或丢失,形成 race condition(竞态条件)

我们可以用 Go 内置的 -race 工具检测:

代码语言:bash


AI代码解释

$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c000114038 by goroutine 14:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:13 +0x78
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 8:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 8 (finished) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Read at 0x00c000114038 by goroutine 14:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x8c
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 24:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 24 (finished) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Read at 0x00c000114048 by goroutine 14:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xb8
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114048 by goroutine 24:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 14 (running) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 24 (finished) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Write at 0x00c000114038 by goroutine 18:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114038 by goroutine 30:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:14 +0x9c
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 18 (running) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 30 (running) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
==================
WARNING: DATA RACE
Write at 0x00c000114048 by goroutine 32:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Previous write at 0x00c000114048 by goroutine 25:
  main.(*Wallet).Transfer()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:15 +0xc8
  main.main.func1()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:28 +0x74
Goroutine 32 (running) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
Goroutine 25 (finished) created at:
  main.main()
      /Users/mac/vscode/mengbin/mengbin92.github.io/_posts/test.go:26 +0x9c
==================
UserA余额: 92
UserB余额: 1769
Found 5 data race(s)
exit status 66

输出会提示存在 DATA RACE,验证了我们的推断。


3. 解决方案一:加锁 —— sync.Mutex

最直接的办法就是用 互斥锁 (sync.Mutex) 保护共享数据,确保在同一时间只有一个 goroutine 能执行转账操作。

代码语言:go


AI代码解释

type Wallet struct {
    Balance int
    mu      sync.Mutex
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
    w.mu.Lock()
    defer w.mu.Unlock()
    if w.Balance >= amount {
        w.Balance -= amount
        target.mu.Lock()
        target.Balance += amount
        target.mu.Unlock()
    }
}

这样,每次转账都必须拿到锁,保证操作的完整性。

再次运行程序,结果稳定为:

代码语言:bash


AI代码解释

UserA余额: 0
UserB余额: 2000

问题解决。


4. 优化方案:读写锁 —— sync.RWMutex

但是,如果钱包的读操作(比如查询余额)非常频繁,而写操作相对较少,这时使用 sync.Mutex 会导致读操作也被阻塞,降低整体性能。

Go 提供了 读写锁 sync.RWMutex

  • 多个读可以并发执行(RLock / RUnlock
  • 写操作依然互斥(Lock / Unlock

修改代码如下:

代码语言:go


AI代码解释

type Wallet struct {
    Balance int
    mu      sync.RWMutex
}
func (w *Wallet) Transfer(amount int, target *Wallet) {
    w.mu.Lock()
    defer w.mu.Unlock()
    if w.Balance >= amount {
        w.Balance -= amount
        target.mu.Lock()
        target.Balance += amount
        target.mu.Unlock()
    }
}
func (w *Wallet) GetBalance() int {
    w.mu.RLock()
    defer w.mu.RUnlock()
    return w.Balance
}

这样:

  • 转账写操作依然是串行的
  • 查询余额可以并发进行,不会相互阻塞
  • 在高并发场景下,系统的整体性能会显著提升

5. 总结

通过这个钱包转账的例子,我们可以总结 Go 并发下的几个关键点:

  1. 数据竞争是并发编程的常见问题,务必通过 -race 检测工具来排查
  2. 互斥锁 Mutex 是解决写并发最直接可靠的方法
  3. 如果读多写少,可以选择 读写锁 RWMutex,提升读取并发能力
  4. 在实际系统中,还要结合业务逻辑,比如数据库事务、分布式锁,保证数据一致性

Go 并发编程的核心是对共享资源的正确管理,合理使用 Mutex 和 RWMutex,才能写出既安全又高效的代码。

本文参考:https://zhuanlan.zhihu.com/p/1945171874297410640

相关文章
|
2月前
|
存储 监控 算法
企业电脑监控系统中基于 Go 语言的跳表结构设备数据索引算法研究
本文介绍基于Go语言的跳表算法在企业电脑监控系统中的应用,通过多层索引结构将数据查询、插入、删除操作优化至O(log n),显著提升海量设备数据管理效率,解决传统链表查询延迟问题,实现高效设备状态定位与异常筛选。
115 3
|
5月前
|
Go
Go语言同步原语与数据竞争:Mutex 与 RWMutex
在Go语言并发编程中,数据竞争是多个goroutine同时读写共享变量且未加控制导致的问题,可能引发程序崩溃或非确定性错误。为解决此问题,Go提供了`sync.Mutex`和`sync.RWMutex`两种同步机制。`Mutex`用于保护临界区,确保同一时间只有一个goroutine访问;`RWMutex`支持多读单写的细粒度控制,适合读多写少场景。使用时需避免死锁,并借助`-race`工具检测潜在的数据竞争,从而提升程序稳定性和性能。
178 51
|
5月前
|
编译器 测试技术 Go
Go语言同步原语与数据竞争:数据竞争的检测工具
本文介绍了 Go 语言中数据竞争(Data Race)的概念及其检测方法。数据竞争发生在多个 Goroutine 无同步访问共享变量且至少一个为写操作时,可能导致程序行为不稳定或偶发崩溃。Go 提供了内置的竞态检测器(Race Detector),通过 `-race` 参数可轻松检测潜在问题。文章还展示了如何使用锁或原子操作修复数据竞争,并总结了在开发和 CI 流程中启用 `-race` 的最佳实践,以提升程序稳定性和可靠性。
|
5月前
|
Go
Go语言同步原语与数据竞争:WaitGroup
本文介绍了 Go 语言中 `sync.WaitGroup` 的使用方法和注意事项。作为同步原语,它通过计数器机制帮助等待多个 goroutine 完成任务。核心方法包括 `Add()`(设置等待数量)、`Done()`(减少计数)和 `Wait()`(阻塞直到计数归零)。文章详细讲解了其基本原理、典型用法(如等待 10 个 goroutine 执行完毕),并提供了代码示例。同时指出常见错误,例如 `Add()` 必须在 goroutine 启动前调用,以及 WaitGroup 不可重复使用。最后总结了适用场景和使用要点,强调避免竞态条件与变量捕获陷阱。
|
5月前
|
安全 Go 调度
Go同步原语与数据竞争:原子操作(atomic)
本文介绍了Go语言中`sync/atomic`包的使用,帮助避免多goroutine并发操作时的数据竞争问题。原子操作是一种不可中断的操作,确保变量读写的安全性。文章详细说明了常用函数如`Load`、`Store`、`Add`和`CompareAndSwap`的功能与应用场景,并通过并发计数器示例展示了其实现方式。此外,对比了原子操作与锁的优缺点,强调原子操作适用于简单变量的高效同步,而不适合复杂数据结构。最后提醒开发者注意使用场景限制,合理选择同步工具以优化性能。
|
9月前
|
测试技术 Go API
Go 切片导致 rand.Shuffle 产生重复数据的原因与解决方案
在 Go 语言开发中,使用切片时由于其底层数据共享特性,可能会引发意想不到的 Bug。本文分析了 `rand.Shuffle` 后切片数据重复的问题,指出原因在于切片是引用类型,直接赋值会导致底层数组共享,进而影响原始数据。解决方案是使用 `append` 进行数据拷贝,确保独立副本,避免 `rand.Shuffle` 影响原始数据。总结强调了切片作为引用类型的特性及正确处理方法,确保代码稳定性和正确性。
247 82
|
数据采集 网络协议 测试技术
使用Go Validator在Go应用中有效验证数据
使用Go Validator在Go应用中有效验证数据
|
JSON 测试技术 Go
零值在go语言和初始化数据
【7月更文挑战第10天】本文介绍在Go语言中如何初始化数据,未初始化的变量会有对应的零值:bool为`false`,int为`0`,byte和string为空,pointer、function、interface及channel为`nil`,slice和map也为`nil`。。本文档作为指南,帮助理解Go的数据结构和正确使用它们。
222 22
零值在go语言和初始化数据
|
存储 算法 Java
Go 通过 Map/Filter/ForEach 等流式 API 高效处理数据
Go 通过 Map/Filter/ForEach 等流式 API 高效处理数据
|
数据采集 缓存 IDE
Go中遇到http code 206和302的获取数据的解决方案
文章提供了解决Go语言中处理HTTP状态码206(部分内容)和302(重定向)的方案,包括如何获取部分数据和真实请求地址的方法,以便程序员能快速完成工作,享受七夕时光。
965 0
Go中遇到http code 206和302的获取数据的解决方案