Go语言高并发编程——互斥锁、条件变量

互斥锁

go语言的sycn包下提供了互斥锁:Mutex。一个互斥锁可以被用来保护一个临界区或者一组相关临界区。我们可以通过它来保证,在同一时刻只有一个 goroutine 处于该临界区之内。

//声明了一个互斥锁
var lock sync.Mutex
lock.Lock()//锁定
task()
lock.Unlock()//解锁

位于lock.Lock()和lock.Unlock()之间的代码块就会被互斥锁保护。

被保护的代码块必须持有锁才能执行。当多个goroutine同时遇到被同一个互斥锁保护的代码块,它们之间就会产生锁竞争,只有抢到锁的goroutine才能去执行代码。抢到锁的goroutine执行完代码后会打开锁,将锁归还,让其它的goroutine继续抢夺。

注意事项:

  1. 使用完锁后一定要解锁(将锁归还),不然就会产生死锁,在必要时,我们可以采用defer来进行解锁。

  2. 不要重复锁定同一把互斥锁,同样会产生死锁。

  3. 不要解锁未锁定的互斥锁,会造成panic。

  4. 互斥锁是结构体类型的,不要进行锁的传递,直接传递会产生一把新锁。

读写锁

读写锁是读 / 写互斥锁的简称。在 Go 语言中,读写锁为sync.RWMutex类型。

读写锁将锁分为了读锁和写锁,如下:

var rwLock sync.RWMutex
//写锁的锁定和解锁
rwLock.Lock()
rwLock.Unlock()

//读锁的锁定和解锁
rwLock.RLock()
rwLock.RUnlock()

单独使用写锁时,它与普通的互斥锁没有区别;单独使用读锁跟不使用锁没有区别。但是当二者同时使用时,在写锁被持有的时候不能获取读锁,同样的,在读锁被持有的时候不能获取写锁。这样保证了读和写操作不会同时进行,读操作不会读到已被修改的值。

总结:

  1. 写锁和写锁之间时互斥的。互斥的意思是同一时间只能被一个goroutine持有。

  2. 读锁和读锁之间是不互斥的。

  3. 读锁和写锁是互斥的。

条件变量

条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。

我们先看一下条件变量的创建:

条件变量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()方法干了几件事情?

  1. 将条件变量对应的锁进行解锁。

  2. 将该goroutine加入cond的通知等待队列,该goroutine进入阻塞状态。

  3. goroutine收到通知后,重新获取获取锁(不需要进行争夺,直接获取)。

注意事项:

  1. 只能在已经获取锁的代码块中调用Wait方法。

  2. 调用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)
}

你可能感兴趣的:(go,高并发,互斥锁)