Go语言并发编程:互斥锁与读写锁详解及实战

小岛来信
小岛来信
发布时间:2026-10-16| 5人浏览过
原创

在Go语言并发编程中,当多个Goroutine同时访问同一块内存资源时,容易引发竞态条件(Race Condition)。为了解决这一安全问题,Go标准库sync包提供了互斥锁Mutex和读写锁RWMutex。本文将通过具体的存取款代码实例,详细讲解这两种锁的使用场景、操作规范及区别。

一、互斥锁Mutex

第1步:理解互斥锁的作用机制

互斥锁用于保护临界区,确保在同一时刻只有一个Goroutine能够进入该区域。它主要包含两个核心操作:Lock()用于加锁,Unlock()用于解锁。

使用注意事项:

  • 严禁重复锁定:重复加锁会导致阻塞甚至死锁(deadlock)。
  • 必须配对解锁:每一个Lock调用都必须有且仅有一个对应的Unlock,通常推荐使用defer mutex.Unlock()来简化逻辑并确保即使发生 panic 也能正确释放。
  • 禁止传递值类型:sync.Mutex是值类型,直接作为参数传递给函数会产生副本,导致原锁状态失效。应传递指针或将其包裹在结构中。
  • 避免未锁定解锁:对未锁定或已解锁的锁执行解锁操作会导致运行时恐慌。

第2步:通过代码演示竞态问题

以下代码模拟了一个账户的初始余额为1000,通过10个协程交替执行存钱和取钱操作。其中protecting变量控制是否启用加锁逻辑,sign通道用于等待所有协程结束。

package main

import (
    "flag"
    "fmt"
    "sync"
)

var (
    mutex   sync.Mutex
    balance int
    protecting uint // 是否加锁,0表示不加锁,1表示加锁
    sign = make(chan struct{}, 10) //通道,用于等待所有goroutine
)

// 存钱
func deposit(value int) {
    defer func() {
        sign <- struct{}{}
    }()

    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("余额: %d\n", balance)
    balance += value
    fmt.Printf("存 %d 后的余额: %d\n", value, balance)
    fmt.Println()
}

// 取钱
func withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()

    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("余额: %d\n", balance)
    balance -= value
    fmt.Printf("取 %d 后的余额: %d\n", value, balance)
    fmt.Println()
}

func main() {
    for i:=0; i < 5; i++ {
        go withdraw(500) // 取500
        go deposit(500)  // 存500
    }

    for i := 0; i < 10; i++ {
        <-sign
    }
    fmt.Printf("当前余额: %d\n", balance)
}

func init() {
    balance = 1000 // 初始账户余额为1000
    flag.UintVar(&protecting, "protecting", 0, "是否加锁,0表示不加锁,1表示加锁")
}

第3步:分析加锁前后的执行结果

不加锁情况(protecting=0):
当不加锁时,多个协程同时读取并修改balance,导致数据错乱。例如,某次取款后余额显示仍为1000,这是因为在读取旧值的同时,其他协程已经修改了余额,造成了竞态条件。

余额: 1000
存 500 后的余额: 1500
余额: 1000
取 500 后的余额: 1000
...
当前余额: 1000

加锁情况(protecting=1):
启用互斥锁后,同一时间只有一个协程能修改余额。虽然输出顺序可能因调度而异,但余额的变化是原子性的,最终结果将保持在预期范围内(1000元),消除了数据竞争。

余额: 1000
取 500 后的余额: 500
余额: 500
存 500 后的余额: 1000
...
当前余额: 1000

二、读写锁RWMutex

第4步:掌握读写锁的设计原理

sync.RWMutex是互斥锁的优化版本,旨在提高“读多写少”场景下的并发性能。它区分了读操作和写操作:

  • 读锁(RLock/RUnlock):允许多个Goroutine同时进行读操作。
  • 写锁(Lock/Unlock):要求独占访问,在此期间既不能进行其他写操作,也不能进行读操作。

兼容性规则:
- 读锁可以与其它读锁共存。
- 写锁与读锁、写锁之间互斥。

第5步:构建带有查询功能的账户结构

为了体现读写锁的优势,我们在之前的例子中增加一个check方法查看余额。使用结构体封装账户信息,并通过指针传递RWMutex以避免复制问题。

package main

import (
    "fmt"
    "sync"
)

type account struct {
    num     uint          // 操作次数
    balance int           // 余额
    rwMu    *sync.RWMutex // 读写锁指针
}

var sign = make(chan struct{}, 15) //通道,用于等待所有goroutine

// 查看余额:使用读锁
func (c *account) check() {
    defer func() {
        sign <- struct{}{}
    }()
    c.rwMu.RLock()
    defer c.rwMu.RUnlock()
    fmt.Printf("%d 次操作后的余额: %d\n", c.num, c.balance)
}

// 存钱:写锁
func (c *account) deposit(value int) {
    defer func() {
        sign <- struct{}{}
    }()
    c.rwMu.Lock()
    defer c.rwMu.Unlock()
    c.num += 1
    fmt.Printf("余额: %d\n", c.balance)
    c.balance += value
    fmt.Printf("存 %d 后的余额: %d\n", value, c.balance)
    fmt.Println()
}

// 取钱:写锁
func (c *account) withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()
    c.rwMu.Lock()
    defer c.rwMu.Unlock()
    c.num += 1
    fmt.Printf("余额: %d\n", c.balance)
    c.balance -= value
    fmt.Printf("取 %d 后的余额: %d\n", value, c.balance)
    fmt.Println()
}

func main() {
    c := account{num: 0, balance: 1000, rwMu: new(sync.RWMutex)}

    // 启动5组存/取操作 + 5次查询,共15个goroutine
    for i:=0; i < 5; i++ {
        go c.withdraw(500) // 取500
        go c.deposit(500)  // 存500
        go c.check()       // 查余额
    }

    for i := 0; i < 15; i++ {
        <-sign
    }
    fmt.Printf("%d 次操作后的余额: %d\n", c.num, c.balance)
}

第6步:验证读写锁的执行效果

运行上述代码,我们可以看到check方法的输出穿插在存钱和取钱操作之间。由于check只持有了读锁,它不会阻塞其他的读操作,也不会被写操作无限期阻塞(但在有写请求排队时,读锁获取可能会稍作等待以保证公平性)。

余额: 1000
取 500 后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
...
10 次操作后的余额: 1000

此例展示了更细粒度的访问控制:写操作(存/取)依然是串行的,但读操作(查余额)可以与其他读操作并行(尽管在此单核演示中体现不明显,在多核高并发下优势显著)。

三、总结与最佳实践

互斥锁和读写锁都是Go语言中解决并发安全的重要手段。选择哪种锁取决于业务场景:

  • 如果读写比例相当或写操作较多,直接使用sync.Mutex即可,逻辑简单且开销小。
  • 如果是典型的读多写少场景(如缓存配置加载、仪表盘数据展示等),使用sync.RWMutex能显著提升吞吐量。

通用原则:
无论使用哪种锁,务必牢记“加锁后必解锁”、“不重复加锁”、“不传递锁的值拷贝”。合理利用defer语句可以有效防止因路径提前退出导致的死锁风险。

以上就是Go语言互斥锁Mutex和读写锁RWMutex的详细内容,更多关于Go语言并发同步的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。