互斥锁
了解互斥锁及其在 Golang 中的实现。
什么是互斥锁(mutex)?
假设我们希望某段代码在同一时间只能被一个协程(goroutine)访问,此时就需要一种加锁和解锁的机制,这种机制可以通过 互斥锁(mutex) 来实现。
我们可以使用互斥锁来锁定和解锁一段代码。被互斥锁保护的代码区域称为临界区(critical section)。互斥锁还可用于保护缓存、状态和寄存器等共享资源,防止多个协程同时访问。
互斥锁用于保护共享资源,具有排他性。如果某个协程获得了互斥锁,那么必须由该协程在操作完成后释放这把锁。
临界区
在并发编程中,多个线程或协程同时访问共享资源可能导致不可预期或错误的行为。临界区是指程序中访问共享变量的一段代码。在临界区内,操作必须是原子性的——即同一时刻只能有一个进程或协程执行这段代码。
临界区可能只包含单个变量,也可以是一段包含若干操作的代码块。
注意:不建议将所有代码都放入临界区。这样做可能会显著影响程序的执行效率。如果所有操作都被放在临界区内,当一个线程获得锁后,其余所有线程都会被阻塞,包括那些与共享资源无关的线程。有些函数执行时间较长,但本身并不涉及共享数据,不会受到并发影响。在这种情况下,若将其也纳入临界区,只会无谓地增加整体运行时间。
代码解析
我们来看一个实际的例子:
1package main 2 3import ( 4 "fmt" 5 "runtime" 6 "sync" 7) 8 9func main() { 10 11 runtime.GOMAXPROCS(4) 12 value, x := 0, 5 13 var wg sync.WaitGroup 14 15 decrement := func() { 16 defer wg.Done() 17 value -= x 18 } 19 20 increment := func() { 21 defer wg.Done() 22 value += x 23 } 24 25 for i := 0; i < 200; i++ { 26 wg.Add(2) 27 go increment() 28 go decrement() 29 } 30 31 wg.Wait() 32 fmt.Println(value) 33 34}
这段代码每次运行的结果都可能不同。原因在于变量 value 不是并发安全的,多个协程同时读写导致了数据竞争(race condition)。总共 400 个协程在争抢修改 value 的值。
如果我们通过将变量操作置于临界区,或使用原子操作来保证线程安全,结果将始终为零。
下面我们看一个并发安全的版本:
1package main 2 3import ( 4 "fmt" 5 "runtime" 6 "sync" 7) 8 9func main() { 10 11 runtime.GOMAXPROCS(4) 12 value, x := 0, 5 13 var wg sync.WaitGroup 14 var mu sync.Mutex 15 16 decrement := func() { 17 defer wg.Done() 18 mu.Lock() 19 value -= x 20 mu.Unlock() 21 } 22 23 increment := func() { 24 defer wg.Done() 25 mu.Lock() 26 value += x 27 mu.Unlock() 28 } 29 30 for i := 0; i < 200; i++ { 31 wg.Add(2) 32 go increment() 33 go decrement() 34 } 35 36 wg.Wait() 37 fmt.Println(value) 38 39}
上述代码每次运行都会输出 0。因为此时每次只有一个协程能够更新 value 的值,消除了多个协程之间的竞争。