[关闭]
@adamhand 2019-02-13T11:44:27.000000Z 字数 1644 阅读 666

golang--Mutex


临界区和竞态

当某个资源可以被多个Go协程共同访问时,这个资源叫做共享资源。这段修改共享资源的代码称为临界区。当多个Go协程同时执行临界区的代码时,就有可能发生竞态

比如下面的代码:

  1. x = x + 1

上述代码的执行过程有三步:

我们假设 x 的初始值为 0。而协程 1 获取 x 的初始值,并计算 x + 1。而在协程 1 将计算值赋值给 x 之前,系统上下文切换到了协程 2。于是,协程 2 获取了 x 的初始值(依然为 0),并计算 x + 1。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 x,因此 x 等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 x,因此在所有协程执行完毕之后,x 都等于 1。

Mutex

Mutex的英文意思是“互斥”。Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。

Mutex 定义了两个方法:Lock 和 Unlock。所有在 Lock 和 Unlock 之间的代码,都只能由一个 Go 协程执行,于是就可以避免竞态条件。

  1. mutex.Lock()
  2. x = x + 1
  3. mutex.Unlock()

如果有一个 Go 协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到 Mutex 解除锁定为止。

竞态问题和Mutex

下面的第一个程序有竞态问题,而第二个程序使用Mutex修复了竞态问题。

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. var i = 0
  7. func incresment(wg *sync.WaitGroup) {
  8. i = i + 1
  9. wg.Done()
  10. }
  11. func main() {
  12. var wg sync.WaitGroup
  13. for x := 0; x < 1000; x++{
  14. wg.Add(1)
  15. go incresment(&wg)
  16. }
  17. wg.Wait()
  18. fmt.Printf("result is %d", i)
  19. }

上面的程序每次输出的结果都不相同,但是大部分时间都不是1000。

下面的程序使用Mutex修复了上述竞态问题。

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. var i = 0
  7. func increment(wg *sync.WaitGroup, lock *sync.Mutex) {
  8. lock.Lock()
  9. i = i + 1
  10. lock.Unlock()
  11. wg.Done()
  12. }
  13. func main() {
  14. var wg sync.WaitGroup
  15. var lock sync.Mutex
  16. for x := 0; x < 1000; x++{
  17. wg.Add(1)
  18. go increment(&wg, &lock)
  19. }
  20. wg.Wait()
  21. fmt.Printf("result is %d", i)
  22. }

上述程序每次输出均为1000。

需要注意的是,传递 Mutex 的地址很重要。如果传递的是 Mutex 的值,而非地址,那么每个协程都会得到 Mutex 的一份拷贝,竞态条件还是会发生。

使用信道处理竞态条件

还可以使用信道来处理竞态问题:

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. var x = 0
  7. func increment(wg *sync.WaitGroup, ch chan bool) {
  8. ch <- true
  9. x = x + 1
  10. <- ch
  11. wg.Done()
  12. }
  13. func main() {
  14. var wg sync.WaitGroup
  15. ch := make(chan bool, 1)
  16. for i := 0; i < 1000; i++{
  17. wg.Add(1)
  18. go increment(&wg, ch)
  19. }
  20. wg.Wait()
  21. fmt.Printf("result is %d", x)
  22. }

因为缓冲信道的容量为1,所以当一个协程向信道中写入true之后没有读出时,其他线程不能向其中写入数据,就会被阻塞,从而达到同一时刻只有一个协程能够执行临界区代码。

Mutex VS 信道

对于这两种方式之间的选择,当 Go 协程需要与其他协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用 Mutex。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注