首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go 并发编程实战:从数据竞争到 Mutex 与读写锁

Go 并发编程实战:从数据竞争到 Mutex 与读写锁

原创
作者头像
孟斯特
发布2025-08-29 13:38:52
发布2025-08-29 13:38:52
2071
举报
文章被收录于专栏:Go学习Go学习

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


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

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

代码语言:go
复制
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
复制
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
复制
UserA余额: 312
UserB余额: 1688

为什么会这样?


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

原因在于:

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

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

代码语言:bash
复制
$ 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
复制
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
复制
UserA余额: 0
UserB余额: 2000

问题解决。


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

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

Go 提供了 读写锁 sync.RWMutex

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

修改代码如下:

代码语言:go
复制
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,才能写出既安全又高效的代码。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 从问题开始:并发转账的数据错乱
  • 2. 问题的根源:数据竞争
  • 3. 解决方案一:加锁 —— sync.Mutex
  • 4. 优化方案:读写锁 —— sync.RWMutex
  • 5. 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档