• 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)
    }
    
  • 相关阅读:
    【JAVA案例】作业管理系统(控制台版本)
    多任务学习
    「PAT乙级真题解析」Basic Level 1005 继续(3n+1)猜想 (问题分析+完整步骤+伪代码描述+提交通过代码)
    js基础语法和代码示例(11-20)
    笨办法学python3
    2022-12-01 mysql列存储引擎-将exists子查询转换为between操作-分析
    天然气潮流计算matlab程序
    3年半测试经验,20K我都没有,看来是时候跳槽了...
    [经验]如何解决python环境中的版本冲突问题
    selinux-policy-default(2:2.20231119-2)软件包内容详细介绍(2)
  • 原文地址:https://blog.csdn.net/m0_62969222/article/details/127103599