前言
在并发编程中,锁是保证数据一致性、避免竞争条件的重要工具。假如有多个线程连续向同一个缓存区写入数据,如果没有一个 机制协调这些线程的写入操作的话,被写入的数据块就可能出现错乱,线程A还没有写完,线程B就开始写入,这样就会造成数据 混乱。那该如何解决呢?
Go
语言提供了 sync.Mutex
和 sync.RWMutex
,用于实现互斥访问共享资源的功能。 这篇文章就来探讨 sync.Mutex
和 sync.RWMutex
的使用方式及区别。
1. sync.Mutex
:互斥锁
sync.Mutex
用于保护共享资源,确保同一时刻只有一个 goroutine
能够访问被锁定的代码段。
基本用法
Mutex
提供了两种主要方法:
Lock()
:加锁,若锁已被其他goroutine
获取,则会阻塞直到该锁被释放。Unlock()
:解锁,释放该锁,使其他等待的 goroutine 能够获取锁。
示例
先来看看,不加锁的代码示例:
go
var counter int
func increment() {
counter++
}
func main() {
var wg sync.WaitGroup
// 启动 100 个 goroutine,每个 goroutine 都会执行 increment 函数
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
这段代码启动了 100 个并发的 goroutine,每个 goroutine 执行 increment
函数来增加 counter
的值。 代码的目的是希望通过并发的方式多次执行 increment
,最终输出 counter
的值。
- 全局变量
counter
:counter
是一个全局整数变量,所有的 goroutine 都会访问和修改这个变量。 increment
函数: 该函数的作用是对counter
进行自增操作,使用counter++
来增加其值。sync.WaitGroup
:sync.WaitGroup
用来等待所有 goroutine 完成。wg.Wait()
会阻塞主 goroutine,直到所有被Add
方法注册的goroutine
都执行完毕。- 启动 100 个 goroutine: 在
main
函数中,使用一个for
循环启动 100 个 goroutine。每个 goroutine 都会执行increment
函数,该函数的目的是将counter
增加 1。
这段代码启动了 100 个 goroutine,期望最终输出的 counter
值是 100。但是你运行代码,发现有时候并不是100,这是出啥问题了呢?
-
竞态条件(Race Condition): 由于多个 goroutine 并发地修改
counter
变量,counter++
并不是原子操作,它由三部分组成:- 读取
counter
的当前值 - 将其加 1
- 将新的值写回
counter
在并发的情况下,如果多个 goroutine 同时执行这三个步骤,它们可能会读取相同的值、加 1 后写回,导致丢失某些增量,最终导致
counter
的值少于预期的 100。例如:
- 假设
counter
当前是 0。 - 两个 goroutine 同时读取到
counter
的值 0,并分别加 1。然后,它们都会写回值 1。 - 结果是
counter
只增加了 1,而不是 2。
- 读取
下面我们改正代码,加锁,解决竞态问题:
go
var counter int
var mu sync.Mutex // 使用互斥锁保护 counter
func increment() {
mu.Lock() // 加锁,防止多个 goroutine 同时修改 counter
counter++
defer mu.Unlock() // 解锁,确保其他 goroutine 可以访问 counter
}
func main() {
var wg sync.WaitGroup
// 启动 100 个 goroutine,每个 goroutine 都会执行 increment 函数
for i := 0; i < 100; i++ {
wg.Add(1) // 增加一个待等待的 goroutine
go func() {
defer wg.Done() // 完成时调用 Done,减少计数
increment()
}()
}
wg.Wait() // 等待所有 goroutine 执行完
fmt.Println("Counter:", counter)
}
这段代码,我们在 increment
函数中使用了sync.Mutex
,
sync.Mutex
锁的使用: 在increment
函数中,使用了一个sync.Mutex
锁来确保每次只有一个 goroutine 修改counter
变量,从而避免了并发冲突和竞态条件。mu.Lock()
用于加锁defer mu.Unlock()
确保在increment
函数执行完成后释放锁。
再次执行代码,最终输出的 counter
值将是 100。
适用场景:
当多个 goroutine
需要对共享资源进行读写操作时,如果每次只有一个 goroutine 可以访问该资源, Mutex
是最适合的选择。
2. sync.RWMutex
:读写锁
sync.RWMutex
是一个比 sync.Mutex
更为细粒度的锁,它允许多个读操作并行进行,但写操作是互斥的。 换句话说,多个读操作可以共享锁,而写操作会阻塞所有读操作和其他写操作。
基本用法
sync.RWMutex
提供了以下方法:
RLock()
:获取读锁,允许多个goroutine
并行获取读锁,但如果有写锁被占用,则阻塞。RUnlock()
:释放读锁。Lock()
:获取写锁,阻塞所有读锁和写锁。Unlock()
:释放写锁。
示例
我们有一个计数器,多个 goroutine
会不断地增加计数器的值。同时,我们也会有一些 goroutine
来读取计数器的当前值。为了提高性能,允许多个 goroutine
并发读取,但写操作则必须是独占的。
go
// Counter 使用读写锁来实现线程安全的计数器
type Counter struct {
mu sync.RWMutex
count int
}
// 增加计数器的值
func (c *Counter) Increment() {
c.mu.Lock() // 获取写锁,阻塞其他读写操作
defer c.mu.Unlock()
c.count++
}
// 读取计数器的值
func (c *Counter) GetValue() int {
c.mu.RLock() // 获取读锁,允许多个 goroutine 并发读取
defer c.mu.RUnlock()
return c.count
}
func main() {
var counter Counter
var wg sync.WaitGroup
// 启动 5 个 goroutine 来增加计数器的值
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d: Incrementing counter\n", id)
counter.Increment()
}(i)
}
// 启动 10 个 goroutine 来读取计数器的值
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d: Current counter value is %d\n", id, counter.GetValue())
}(i)
}
wg.Wait() // 等待所有 goroutine 执行完成
// 输出最终的计数器值
fmt.Printf("Final counter value: %d\n", counter.GetValue())
}
这段代码中:
Counter
结构体:count
用来存储计数器的值。mu
是一个sync.RWMutex
类型的读写锁,保证了对count
的并发读写操作是线程安全的。Increment
方法: 获取写锁c.mu.Lock()
,这意味着在执行增量操作时,其他的读操作和写操作都会被阻塞,直到当前操作完成。 完成后释放锁:defer c.mu.Unlock()。GetValue
方法: 获取读锁c.mu.RLock()
,允许多个 goroutine 并发读取计数器的值,但在获取读锁期间无法进行写操作。 完成后释放读锁:defer c.mu.RUnlock()
。main
函数: 创建了多个goroutine
来读取计数器的值,并且启动了多个goroutine
来增加计数器的值。 使用sync.WaitGroup
来等待所有goroutine
执行完成。
适用场景:
如果共享资源需要频繁读取且不常写入,使用 RWMutex
可以提高并发性能。多个 goroutine
可以并行读取数据,而只有在需要写操作时才会阻塞
最后
互斥锁和读写锁整个使用起来还是比较简单的,但想要用的好,还得结合业务场景,多思考。