go语言的sycn包下提供了互斥锁:Mutex。一个互斥锁可以被用来保护一个临界区或者一组相关临界区。我们可以通过它来保证,在同一时刻只有一个 goroutine 处于该临界区之内。
//声明了一个互斥锁
var lock sync.Mutex
lock.Lock()//锁定
task()
lock.Unlock()//解锁
位于lock.Lock()和lock.Unlock()之间的代码块就会被互斥锁保护。
被保护的代码块必须持有锁才能执行。当多个goroutine同时遇到被同一个互斥锁保护的代码块,它们之间就会产生锁竞争,只有抢到锁的goroutine才能去执行代码。抢到锁的goroutine执行完代码后会打开锁,将锁归还,让其它的goroutine继续抢夺。
注意事项:
使用完锁后一定要解锁(将锁归还),不然就会产生死锁,在必要时,我们可以采用defer来进行解锁。
不要重复锁定同一把互斥锁,同样会产生死锁。
不要解锁未锁定的互斥锁,会造成panic。
互斥锁是结构体类型的,不要进行锁的传递,直接传递会产生一把新锁。
读写锁是读 / 写互斥锁的简称。在 Go 语言中,读写锁为sync.RWMutex
类型。
读写锁将锁分为了读锁和写锁,如下:
var rwLock sync.RWMutex
//写锁的锁定和解锁
rwLock.Lock()
rwLock.Unlock()
//读锁的锁定和解锁
rwLock.RLock()
rwLock.RUnlock()
单独使用写锁时,它与普通的互斥锁没有区别;单独使用读锁跟不使用锁没有区别。但是当二者同时使用时,在写锁被持有的时候不能获取读锁,同样的,在读锁被持有的时候不能获取写锁。这样保证了读和写操作不会同时进行,读操作不会读到已被修改的值。
总结:
写锁和写锁之间时互斥的。互斥的意思是同一时间只能被一个goroutine持有。
读锁和读锁之间是不互斥的。
读锁和写锁是互斥的。
条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。
我们先看一下条件变量的创建:
条件变量Cond是一个结构体类型的,sync包下的NewCond()函数可以返回一个条件变量的指针:func NewCond(l Locker) *Cond
,他需要一个Locker类型的参数。
Locker是一个接口,如下:
type Locker interface {
Lock()
Unlock()
}
互斥锁就有这两个方法:
func (m *Mutex) Lock() {}
func (m *Mutex) Unlock() {)
我们需要注意,并不是Mutex实现了该接口,而是*Mutex实现了该接口。
分析了相关源码,我们直到了如何创建一个条件变量,如下:
var lock sync.Mutex
sync.NewCond(&lock)
条件变量中的常用方法:
cond := sync.NewCond(&lock)
cond.Wait() ------ 让当前goroutine进入该条件变量的通知等待队列
cond.Signal() ------- 向该条件变量的消息等待队列中的一个goroutine发送通知
cond.Broadcast()-----向该条件变量的消息等待队列中的所有goroutine发送通知
条件变量并不是被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。
例如,goroutineA,B同时去抢夺一把互斥锁。A成功抢到了互斥锁,但是当A在执行任务时发现不满足执行任务的资源条件,那么就可以让goroutineA加入条件变量的消息等待队列,并且将锁释放掉。这时B拿到了锁,通过B的执行改变了资源条件,B执行完之后会通知A,收到通知的A就会重新获取锁,执行任务代码。
我们用伪代码演示该过程:
goroutineA:
//创建锁和条件变量
var lock sync.Mutex
cond := sync.NewCond(&lock)
//goroutineA
lock.Lock()
for(resources == 0){//不满足
cond.Wait()
}
resources == 1//执行任务代码
lock.Ulock()
//goroutine B
lock.Lock()
//改变资源条件
lock.Ulock()
cand.Signal()//通知
Wait()方法干了几件事情?
将条件变量对应的锁进行解锁。
将该goroutine加入cond的通知等待队列,该goroutine进入阻塞状态。
goroutine收到通知后,重新获取获取锁(不需要进行争夺,直接获取)。
注意事项:
只能在已经获取锁的代码块中调用Wait方法。
调用Wait方法的条件变量对应的锁必须是和锁着该代码块的锁是同一把锁。
为什么要要使用for(resources == 0),而不是if(resources == 0)?
使用if时,对资源条件的检查只能进行一次,而使用for可以多次判断。如果当前goroutine被通知,但是此时的资源条件仍然不满足,如果程序进行下去就会出错。所以在这里使用for
下面我们用互斥锁和条件变量实现一个生产消费者模式:
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
// 仓库类型
type event1 struct {
queue [10]string //存放产品的队列
num int //剩余产品数量
}
var (
lock sync.Mutex //锁
cond = sync.NewCond(&lock) //条件变量
//产品仓库
resources = event1{
num: 0,
}
wg sync.WaitGroup
)
// 生产者
func (event *event1) producer(id int) {
for i := 1; i <= 10; i++ {
cond.L.Lock()
for event.num == 10 {
cond.Wait() //仓库已满,将该goroutine挂起,等待被通知
}
//生产产品
str := strconv.Itoa(id) + "-" + strconv.Itoa(i)
//将产品装入仓库
event.queue[event.num] = str
//计数
event.num++
fmt.Println("生产者生产了" + str)
cond.L.Unlock()
//该goroutine已解锁,通知在通知等待队列的goroutine获取锁
cond.Signal()
}
wg.Done()
}
func (event *event1) consumer() {
for i := 1; i <= 10; i++ {
cond.L.Lock()
for event.num == 0 {
cond.Wait()
}
event.num--
fmt.Println("消费者消费了", event.queue[event.num])
cond.L.Unlock()
cond.Signal()
}
wg.Done()
}
func main() {
start := time.Now().Unix()
fmt.Printf("start:%d\n", start)
//启动10个生产者goroutine
for i := 1; i <= 10; i++ {
go resources.producer(i)
wg.Add(1)
}
//启动10个消费者goroutine
for i := 1; i <= 10; i++ {
go resources.consumer()
wg.Add(1)
}
wg.Wait()
end := time.Now().Unix()
fmt.Printf("end:%d\n", end)
fmt.Printf("花费时间:%d", end-start)
}