Go语言提供了丰富的同步原语来处理并发编程中的共享资源访问问题。其中最基础也最常用的就是互斥锁(Mutex)和读写锁(RWMutex)。
1. sync.Mutex(互斥锁)
Mutex核心特性
- 互斥性/排他性:同一时刻只有一个goroutine能持有锁
- 不可重入:同一个goroutine重复加锁会导致死锁
- 零值可用:
sync.Mutex
的零值就是未锁定的互斥锁 - 非公平锁:不保证goroutine获取锁的顺序
Mutex例子
例1:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wait sync.WaitGroup
var count = 0
var lock sync.Mutex
func main() {
wait.Add(10)
for i := 0; i < 10; i++ {
go func(data *int) {
// 加锁
lock.Lock()
// 模拟访问耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
// 访问数据
temp := *data
// 模拟计算耗时
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
ans := 1
// 修改数据
*data = temp + ans
// 解锁
lock.Unlock()
fmt.Println(*data)
wait.Done()
}(&count)
}
wait.Wait()
fmt.Println("最终结果", count)
}
输出:
1
2
3
4
5
6
7
8
9
10
最终结果 10
解读:
- lock 是一个互斥锁,用于确保在任何时刻只有一个 goroutine 可以访问和修改 count 变量,防止数据竞争。
- 每个 goroutine 首先通过 lock.Lock() 加锁,确保在同一时间只有一个 goroutine 可以修改 count。
- time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) 模拟了对数据的访问和计算耗时,这里的随机数生成器用于在每次循环中生成一个 0 到 999 之间的随机整数,作为睡眠的时间
- wait.Wait() 阻塞主 goroutine,直到等待组中的所有 goroutine 都完成任务。
- fmt.Println("最终结果", count) 打印 count 的最终值。
在 Go 语言中,func(data *int) 这样的写法是用来定义一个匿名函数,并且该匿名函数接受一个参数,参数类型是指向整型的指针。在这段代码的目的是在并发环境中对一个共享变量 count 进行修改,以避免数据竞争。
- go func(data *int) { ... }(&count) 这里的 go 关键字用于启动一个新的 goroutine。
- func(data *int) { ... } 是一个匿名函数,它接受一个参数 data,这个参数是一个指向整型的指针。
- (&count) 表示传递给匿名函数的参数是 count 变量的地址。通过传递指针,匿名函数可以直接访问和修改 count 的值。
例2
package main
import (
"fmt"
"sync"
"time"
)
var (
counter int
lock sync.Mutex
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
func increment(wg *sync.WaitGroup) {
defer wg.Done()
lock.Lock() // 加锁
defer lock.Unlock() // 使用defer确保解锁
// 临界区
temp := counter
time.Sleep(1 * time.Millisecond)
counter = temp + 1
}
输出:
Final counter: 10
解读:
- var wg sync.WaitGroup:声明了一个等待组wg,用于等待所有goroutine完成。
- wg.Add(1):为每次循环增加一个等待计数。
- go increment(&wg):启动goroutine运行increment函数,并传入等待组的地址。
- wg.Wait():等待所有等待计数为零,即所有goroutine完成。
- defer wg.Done():使用defer关键字确保函数执行完毕后调用wg.Done(),减少等待组的一个计数。
- lock.Lock():在函数执行前加锁,防止多个goroutine同时访问counter。
- defer lock.Unlock():同样使用defer关键字确保函数执行完毕后解锁。
- 临界区代码段:将counter的值赋给temp,休眠1毫秒,然后将counter设置为temp + 1。这里通过休眠模拟了一个耗时操作。
2. sync.RWMutex(读写锁)
Go 中读写互斥锁的实现是 sync.RWMutex
,它也同样实现了 Locker
接口,但它提供了更多可用的方法,如下:
// 加读锁
func (rw *RWMutex) RLock()
// 非阻塞地尝试加读锁 (Go 1.18+)
func (rw *RWMutex) TryRLock() bool
// 解读锁
func (rw *RWMutex) RUnlock()
// 加写锁
func (rw *RWMutex) Lock()
// 非阻塞地尝试加写锁 (Go 1.18+)
func (rw *RWMutex) TryLock() bool
// 解写锁
func (rw *RWMutex) Unlock()
1. RWMutex基本概念
读写锁的特点
- 并发读:多个goroutine可以同时持有读锁
- 互斥写:写锁是排他的,同一时间只能有一个goroutine持有写锁
- 写优先:当有写锁等待时,新的读锁请求会被阻塞,防止写锁饥饿
与Mutex的区别
特性 | Mutex | RWMutex |
---|---|---|
并发读 | 不支持 | 支持多个goroutine同时读 |
并发写 | 不支持 | 不支持 |
性能 | 一般 | 读多写少场景性能更好 |
复杂度 | 简单 | 相对复杂 |
2. RWMutex的工作原理
锁状态
- 当写锁被持有时:所有读锁和写锁请求都会被阻塞
- 当读锁被持有时:新的读锁请求可以立即获得锁,写锁请求会被阻塞
- 当写锁请求等待时:新的读锁请求会被阻塞(写优先)
内部实现要点
- 读者计数:记录当前持有读锁的goroutine数量
- 写者标记:标识是否有goroutine持有或等待写锁
- 写者信号量:用于唤醒等待的写者
- 读者信号量:用于唤醒等待的读者
3. RWMutex的例子
线程安全的缓存实现
type Cache struct {
mu sync.RWMutex
items map[string]interface{}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, found := c.items[key]
return item, found
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
解读
Cache 结构体
- items:一个映射(map),键为字符串,值为接口类型(interface{}),用于存储缓存数据。
- mu:一个sync.RWMutex实例,用于控制对items的并发访问。
Get 方法:
- c.mu.RLock():获取读锁,允许多个读协程同时访问items。
- defer c.mu.RUnlock():确保在函数返回前释放读锁。
- item, found := c.items[key]:从items中获取指定key对应的值,并判断该key是否存在。
- return item, found:返回获取的值和是否找到的布尔值。
Set 方法:
- c.mu.Lock():获取写锁,确保只有一个写协程可以访问items。
- defer c.mu.Unlock():确保在函数返回前释放写锁。
- c.items[key] = value:将指定key对应的值设置为value。
Delete 方法:
- c.mu.Lock():获取写锁,确保只有一个写协程可以访问items。
- defer c.mu.Unlock():确保在函数返回前释放写锁。
- delete(c.items, key):从items中删除指定key对应的键值对。
3.互斥锁和读写锁的区别和应用场景
核心区别对比
特性 | 互斥锁(Mutex) | 读写锁(RWMutex) |
---|---|---|
并发读 | 完全互斥,读操作也需要独占锁 | 允许多个goroutine同时持有读锁 |
并发写 | 互斥,同一时间只有一个写操作 | 互斥,同一时间只有一个写操作 |
锁类型 | 单一锁类型 | 区分读锁(RLock)和写锁(Lock) |
性能开销 | 较高(所有操作都互斥) | 读操作开销低,写操作开销与Mutex相当 |
实现复杂度 | 简单 | 相对复杂 |
适用场景 | 读写操作频率相当或写多读少 | 读操作远多于写操作的场景 |
选择场景
优先考虑RWMutex当:
- 读操作次数是写操作的5倍以上
- 读操作临界区较大(耗时较长)
- 需要支持高频并发读取
选择Mutex当:
- 读写操作频率相当(写操作占比超过20%)
- 临界区非常小(几个CPU周期就能完成)
- 代码简单性比极致性能更重要
- 需要锁升级/降级(虽然Go不支持,但Mutex更不容易出错)
特殊考虑:
- 对于极高性能场景,可考虑
atomic
原子操作 - 对于复杂场景,可考虑
sync.Map
或分片锁
- 对于极高性能场景,可考虑