一、前言
go语言为了保证多goroutine对同一个共享资源进行并发读写,提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁、CAS、原子变量操作类,本节我们先来看看go中互斥锁
二、互斥锁
在go中互斥锁是在sync包提供的,其是独占锁,同时只有一个线程可以获取该锁。当一个goroutine持有互斥锁时候,其他请求获取锁的goroutine会被阻塞挂起,等获取锁的goroutine释放锁后,阻塞的线程中的一个才可以被唤醒并获取锁。同时该锁是不可重入锁,当前goroutine在获取该锁后释放前再次尝试获取该锁会被阻塞。
在go中使用 var mutex sync.Mutex就可以创建一个开箱即用的互斥锁,调用mutex.Lock()尝试获取锁,调用mutex.Unlock()释放锁,需要注意的是释放锁时候要保证某一个goroutine已经获取到了锁,如果在没有获取锁的goroutinemutex.Unlock()的情况下,会抛出错误:
另外需要注意的是释放锁的goroutine不要求与获取锁的goroutine是同一个:
package main
import (
"sync"
)
var (
mutex sync.Mutex //互斥锁
wg sync.WaitGroup
)
func main() {
wg.Add(1)
//新goroutine获取锁
go func() {
mutex.Lock()
wg.Done()
}()
wg.Wait()
//主goroutine释放锁
mutex.Unlock()
}
如上代码在新开启的goroutine内获取锁,在主goroutine内释放锁是OK的,也就是互斥锁是与具体goroutine无关的,这与Java中的互斥锁不同,Java中获取锁的线程是要记录锁的持有者,只有获取到锁的线程才可以释放锁。
另外go中互斥锁分为两种模式:正常与饥饿模式。
正常模式:
在正常模式中当多个goroutine同时获取同一个互斥锁时候,其中一个goroutine会获取到该锁,其他的goroutine则会被放入到与该锁维护的一个FIFO队列里面,然后被阻塞挂起。
当获取锁的goroutine释放锁后,该队列里面的某个等待的goroutine会被激活,激活的goroutine并不能马上获取到锁的所有权,这是因为当前可能有新创建的大量goroutine也需要获取该锁,这时候激活的goroutine就要与这些新的goroutine共同竞争锁的所有权,所以被激活的goroutine还是很有可能获取不到锁,如果发生了这种情况,则被激活的线程还是要被放到队列最前面(队首),最后如果一个被阻塞的goroutine尝试了1ms的时间还没获取到锁,则互斥锁的模式会被自动切换到饥饿模式
饥饿模式
在饥饿模式下当多个goroutine同时获取同一个互斥锁时候,其中一个goroutine会获取到该锁,其他的goroutine则会被放入到与该锁维护的一个FIFO队列里面,然后被阻塞挂起
当获取锁的goroutine释放锁后,锁的所有权会被交给FIFO队列里面队首的goroutine,新创建的goroutine尝试获取锁的时候,即使发现了当前锁是空闲的也不会去尝试获取,而是被放到锁做维护的FIFO的队尾部。
如果一个激活的goroutine接受了锁的所有权,并且当发生了下面的某一个事情时候该锁从饥饿模式切换回正常模式:1.当前激活的goroutine是FIFO队列最后一个,1.当前激活的goroutine在1ms时间内就获取到了锁;
锁的状态转换可以使用下面图来展示:
总:正常模式可以提供更好的性能,因为一个goroutine可以在有goroutine处于等待锁的条件下抢占式的去获取锁,而不需要遵循先来先到的原则;而饥饿模式则可以很好的预防锁的FIFO队列队尾部的goroutine一直获取不到锁的情况。
三、使用互斥锁实现原子计数器
本节我们使用独占锁来实现一个线程安全的计数器:
package main
import (
"fmt"
"sync"
)
var (
counter int//计数器
wg sync.WaitGroup//信号量
mutex sync.Mutex//互斥锁
)
func main() {
//1.两个信号量
wg.Add(2)
//2.开启两个线程
go incCounter()
go incCounter()
//3.等待子线程结束
wg.Wait()
fmt.Println(counter)
}
func incCounter() {
defer wg.Done()
//2.1.获取锁
mutex.Lock()
//2.2.计数加1
counter++
//2.3.释放独占锁
mutex.Unlock()
}
在go中使用 sync.Mutex 就可以获取一个开箱即用的互斥锁
counter是一个变量,这里用来存放计数
wg用来实现主线程等待子线程执行完毕,代码(2)设置信号为2,因为本例子里面开启了两个字线程
代码(2)开启了两个子线程,线程内首先获取互斥锁,然后累加计数,然后释放锁,最后递减信号量
代码(3)等待子线程执行完毕后返回,然后打印计数
三、总结
go中互斥锁是独占锁,并且是不可重入锁,同一个线程并不可获取同一个锁多次,并且其锁模式提供了正常与饥饿模式,并在一定情况下自动进行转换,提高了锁的性能。