一文掌握Mutex | Go主题月

626 阅读4分钟

首先来了解几个概念

临界区

在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。 临界区就是一个被共享的资源,或者说是一个整体的一组共享资源,比如对数据库的访问、对某一个共享数据结构的操作、对一个 I/O 设备的使用、对一个连接池中的连接的调用,等等。

当多个线程同步访问临界区,就会出现访问或者操作上的错误,所以这时候就需要互斥锁,来界定临界区只能同时还由一个线程持有。

同步原语

同步原语是解决并发问题的一个基础数据结,在Golang中,互斥锁 Mutex、读写锁 RWMutex、并发编排 WaitGroup、条件变量 Cond、Channel等都是同步原语

同步原语一般用在如下场景:

  • 共享资源。并发读写就会出现数据竞争(data race)。使用Mutex、RWMutex
  • 任务编排。需要 goroutine 按照一定的规律执行,而 goroutine 之间有相互等待或者依赖的顺序关系,可以用 WaitGroup 或者 Channel 来实现。
  • 消息传递。信息交流以及不同的 goroutine 之间的线程安全的数据交流,可以使用 Channel 来实现。

Mutex 的基本使用方法

Mutex 可能处于两种操作模式下:正常模式和饥饿模式。

  • 正常模式:所有协程以先进先出(FIFO)方式进行排队,被唤醒的协程同样需要竞争方式争夺锁,新协程争抢会有优势,因为他们已经运行在CPU上,更容易抢到锁,如果一个协程在等待超过1毫秒会自动切换到饥饿模式下。
  • 饥饿模式:互斥锁会直接由解锁的协程交给队列头部的等待者,新争抢者不能直接获得锁,不尝试自旋,它会乖乖地加入到等待队列的尾部。

Mutex的主要方法,由Lock()和Unlonk()方法组成,使用Lock()加锁后便不能再次对其加锁操作,直到Unlock()解锁后才能再次加锁,适用于读写不确定的场景,并且只允许只有一个读或者写的场景。

image.png

原子性,把一个互斥量锁定为一个原子操作,保证如果一个协程锁定了一个互斥量,这时候其他协程同一时间不能成功锁定这个互斥量。 唯一性:如果一个协程锁定了一个互斥量,在他解锁之前,其他协程无法锁定这个互斥量。 互斥锁只能锁定一次,当在解锁之前再次进行加锁,便会无法加锁。如果在加锁前解锁,便会报错"panic: sync: unlock of unlocked mutex"。  互斥锁无冲突,有冲突时,首先自旋,经过短暂自旋后可以获得锁,如果自旋无结果时通过信号通知协程继续等待。

首先我们来看一个例子

package main

import (
	"fmt"
	"sync"
)

func main() {
	var count = 0
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				count++
			}
		}()
	}
	wg.Wait()
	fmt.Println(count)
}

应该都知道这个输出不会是10000. 因为count++不是原子操作,会发生并发问题。

Go官方提供了一个检测并发访问共享资源是否有问题的工具: race detector,它可以帮助我们自动发现程序有没有 data race 的问题。

在编译(compile)、测试(test)或者运行(run)Go 代码的时候,加上 race 参数,就有可能发现并发问题。

go run -race main.go

当出现data,race的时候,就需要用到Mutex了。我们在count++ 前后进行加锁解锁就可以得到想要的结果了。

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	var count = 0
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				mu.Lock()
				count++
				mu.Lock()
			}
		}()
	}
	wg.Wait()
	fmt.Println(count)
}

常见的 4 种错误场景

  • Lock/Unlock 不是成对出现

  • 重入

  • 死锁

  • Copy 已使用的 Mutex Package sync 的同步原语在使用后是不能复制的。Mutex 是一个有状态的对象,它的 state 字段记录这个锁的状态。如果你要复制一个已经加锁的 Mutex 给一个新的变量,那么新的刚初始化的变量居然被加锁了.

    解决办法:Go 在运行时,有死锁的检查机制(checkdead() 方法),它能够发现死锁的 goroutine。 使用go vet 检测死锁

    go vet main.go