Go语言互斥锁Mutex与读写锁RWMutex用法详解

季宁
季宁
发布时间:2026-10-16 | 5人浏览过
原创

Go语言中的协程并发需要严格的同步机制来保护共享资源。本文详细解析了sync.Mutex(排他锁)与sync.RWMutex(读写锁)的实现原理与使用场景。通过具体的代码示例,展示了如何利用Lock/Unlock和RLock/RUnlock操作构建安全的临界区,避免数据竞争,并分析了在高频读取场景下RWMutex相较于Mutex的性能优势。

sync.Mutex:基础排他锁

在Go语言中,sync.Mutex是最基础的互斥锁类型,用于实现排他性访问。其核心逻辑简单且严格:

  • 互斥性:Lock()获取锁,Unlock()释放锁。一旦锁被持有,其他请求Lock()的goroutine将被阻塞,直到锁被释放。
  • 无读写区分:Mutex不区分读操作和写操作,任何类型的访问都视为互斥。这意味着即使是只读操作,也会阻塞其他正在进行的读写操作。
  • 临界区(Critical Section):Lock()与Unlock()之间的代码块被称为临界区。该区域内的代码保证在同一时刻只有一个goroutine能够执行,从而实现线程安全。

第1步:使用Mutex保护共享变量

当多个协程同时修改同一个全局变量时,必须使用Mutex进行保护,否则会导致数据竞争(Race Condition)。以下是标准的保护模式:

package main

import (
	"fmt"
	"sync"
	"time"
)

// 共享变量
var (
	m  sync.Mutex
	v1 int
)

// 修改共享变量
func change(i int) {
	m.Lock()
	// 临界区开始
	time.Sleep(time.Second)
	v1 = v1 + 1
	if v1%10 == 0 {
		v1 = v1 - 10*i
	}
	// 临界区结束
	m.Unlock()
}

// 访问共享变量
func read() int {
	m.Lock()
	a := v1
	m.Unlock()
	return a
}

第2步:分析并发执行的随机性

在实际并发环境中,goroutine的调度顺序是不确定的。以下测试展示了即使有锁保护,输出顺序仍然可能是随机的:

func main() {
	var numGR = 21
	var wg sync.WaitGroup

	fmt.Printf("%d", read())

	// 创建21个goroutine并发执行
	for i := 0; i < numGR; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			change(i)
			fmt.Printf(" -> %d", read())
		}(i)
	}
	wg.Wait()
}

关键点说明:

  1. 竞争现象:虽然锁保证了临界区的独占性,但42个临界区(21次修改+21次读取)的执行顺序是随机的。
  2. 状态依赖:由于change函数中包含基于v1%10==0的逻辑分支,后续执行到的goroutine可能会根据当前v1的值进行大幅调整(如减去i*10),导致数值出现跳跃。
  3. 非配对执行read()并不一定紧跟在某个特定的change()之后。一个goroutine的change结束后,另一个goroutine的change可能先运行,导致read读取的是他人的修改结果。

如果忘记调用Unlock(),程序将陷入永久死锁。因此,最佳实践是使用defer m.Unlock()确保锁最终会被释放。

适合sync.Mutex的数据结构

对于内置类型的单独变量加锁并不符合封装原则。合理的做法是将锁嵌入到包含数据的结构体中:

type SafeCounter struct {
	mu   sync.Mutex
	value int
}

func (sc *SafeCounter) Inc() {
	sc.mu.Lock()
	defer sc.mu.Unlock()
	sc.value++
}

这样,任何对该结构体字段的访问都需要先锁定对象本身,逻辑更加清晰且不易出错。

sync.RWMutex:读写分离锁

当业务场景呈现“读多写少”的特征时,sync.Mutex会因为串行化所有访问而成为性能瓶颈。sync.RWMutex通过区分读锁和写锁解决了这一问题。

1. 锁的特性对比

  • 读锁-读锁兼容:允许任意数量的goroutine同时持有读锁进行读取。
  • 读锁-写锁互斥:写锁申请时会阻塞,直到所有读锁释放;反之,当写锁持有时,新的读锁申请也会被阻塞。
  • 写锁-写锁互斥:同一时间只能有一个写者。

第3步:实现读写分离的代码示例

通过比较两种不同锁策略的表现,可以直观看到RWMutex的优势:

type secret struct {
	RWM      sync.RWMutex
	M        sync.Mutex
	password string
}

// RWMutex读模式:允许多个并发读取
func rwMutexShow(c *secret) string {
	c.RWM.RLock()
	defer c.RWM.RUnlock()
	// 模拟耗时操作
	time.Sleep(1 * time.Second)
	return c.password
}

// Mutex读模式:强制串行读取
func mutexShow(c *secret) string {
	c.M.Lock()
	defer c.M.Unlock()
	time.Sleep(1 * time.Second)
	return c.password
}

// 写模式:两者一致
func Change(c *secret, pass string) {
	c.RWM.Lock()
	defer c.RWM.Unlock()
	time.Sleep(3 * time.Second)
	c.password = pass
}

第4步:测试并发行为

在main函数中启动5个读取goroutine和1个写入goroutine:

func main() {
	Password := &secret{password: "myPassword"}
	var wg sync.WaitGroup

	// 启动5个读取协程
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println(rwMutexShow(Password))
		}()
	}
	// 启动1个写入协程
	wg.Add(1)
	go func() {
		defer wg.Done()
		Change(Password, "newSecret")
	}()
	wg.Wait()
}

执行结果分析:

  • 使用RWMutex时:5个读取goroutine几乎可以同时进入临界区(因为读锁兼容),虽然每个需等待1秒,但它们在系统层面是并行执行的,总耗时较短。当写goroutine获得写锁后,其余读取任务会排队等待。
  • 使用Mutex时:所有5个读取goroutine必须排队依次执行,总耗时将是单个读取耗时的5倍,显著降低了吞吐量。

用Mutex还是用RWMutex

选择哪种锁取决于具体的访问模式:

  1. 读写比例低(写频繁):直接使用sync.Mutex。复杂的读写锁内部实现开销较大,如果写操作很频繁,Mutex往往更高效且更不易出错。
  2. 读多写少:首选sync.RWMutex。它能极大提升并发系统的读取吞吐量。

注意事项:无论是Mutex还是RWMutex,锁都不绑定特定的goroutine。一个goroutine获得的锁可以由另一个goroutine释放(只要拥有该锁变量的引用即可),但这通常不是推荐的编码风格,容易导致逻辑混乱。

以上就是Go语言协程互斥锁Mutex和读写锁RWMutex用法实例详解的详细内容,更多关于Go语言并发编程的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表