golang中sync.Cond的理解

58 阅读2分钟
package study

import (
	"container/list"
	"fmt"
	"sync"
	"testing"
	"time"
)

/*
比如在消费者-生产者模型中,只有在队列中有资源时,消费者才可以消费,只有在队列还能够放入资源时,
生产者才可以放入资源。消费者协程和生产者协程之间需要同步队列的状态,这里的状态同步可以用sync.Cond来实现。

这是一种通用的编程模式:

获取锁
while(条件不满足){
	释放锁
	线程/协程阻塞等待,直到收到条件满足的通知
	获取锁
}
临界区操作
释放锁
*/

var (
	lock  = new(sync.Mutex)
	cond  = sync.NewCond(lock)
	queue = NewQueue(5)
	done  = make(chan bool)
)

type Product struct {
	Data string
}

type Queue struct {
	l   *list.List
	Cap int
}

func NewQueue(capacity int) *Queue {
	return &Queue{l: list.New(), Cap: capacity}
}

func (q *Queue) Put(p *Product) {
	q.l.PushBack(p)
}

func (q *Queue) Take() *Product {
	t := q.l.Front()
	defer q.l.Remove(t)
	return t.Value.(*Product)
}

func (q *Queue) IsEmpty() bool {
	return q.l.Len() == 0
}

func (q *Queue) IsFull() bool {
	return q.l.Len() == q.Cap
}

//一个cond的底层有两把锁,然后增加了一些唤醒的操作。
//【一把用户态的锁,用来保证用户态的临界区代码,一把runtime的锁,用来保护等待和唤醒队列】【waite,signal,notifiyall都会完成runtime锁的加锁和释放】
// 思考:为什么需要两把锁,runtime的锁用于保护等待队列是必要的,用户态的锁是必要的吗?
//【用户态的锁不是必要的,但是提供了临界代码保护的能力,如果不需要这种能力,可以在wait方法后面紧跟着就unlock】
//【runtime的通知机制,通过原子自增的ticket实现,保证最先wait的一定能够最新notify,具体原理看:https://blog.csdn.net/qq_39397165/article/details/113856138】

//主动lock的会自动抢锁
//wait的,在解锁以后会挂在cond下
//notify会从挂在cond下的g中拿一个去lock下和其他lock的一起抢锁
//notify all 会把所有挂在cond下的g都拿去解锁
func TestCond(t *testing.T) {
	for i := 0; i < 3; i++ {
		go func(i int) {
			for {
				lock.Lock()
				fmt.Println("消费者上锁")
				for queue.IsEmpty() {
					fmt.Println("消费者解锁 waite")
					cond.Wait()
					fmt.Println("消费者上锁")
				}
				fmt.Printf("消费者[%d]:%s\n", i, queue.Take().Data)
				cond.Signal()
				fmt.Println("消费者通知生产者")
				time.Sleep(5 * time.Second)
				fmt.Println("消费者解锁")
				lock.Unlock()
			}
		}(i)
		//time.Sleep(1 * time.Second)
	}
	for i := 0; i < 5; i++ {
		go func(i int) {
			for {
				lock.Lock()
				fmt.Println("生产者上锁")
				for queue.IsFull() {
					fmt.Println("生产者解锁 waite")
					cond.Wait()
					fmt.Println("生产者上锁")
				}
				fmt.Printf("生产者[%d]\n", i)
				queue.Put(&Product{Data: "hello, world"})
				cond.Signal()
				fmt.Println("生产者通知消费者")
				time.Sleep(5 * time.Second)
				fmt.Println("生产者解锁")
				lock.Unlock()
				time.Sleep(1 * time.Second)
			}
		}(i)
		//time.Sleep(1 * time.Second)
	}
	<-done

}

/*
有四个worker和一个master,worker等待master去分配指令,master一直在计数,计数到5的时候通知第一个worker,计数到10的时候通知第二个和第三个worker。
首先列出几种解决方式
1、所有worker循环去查看master的计数值,计数值满足自己条件的时候,触发操作 >>>>>>>>>弊端:无谓的消耗资源
2、用channel来实现,几个worker几个channel,eg:worker1的协程里<-channel(worker1)进行阻塞,计数值到5的时候,给worker1的channel放入值,
阻塞解除,worker1开始工作。 >>>>>>>弊端:channel还是比较适用于一对一的场景,一对多的时候,需要起很多的channel,不是很美观
3、用条件变量sync.Cond,针对多个worker的话,用broadcast,就会通知到所有的worker。
*/
func TestCondQueue(t *testing.T) {
	mutex := sync.Mutex{}
	var cond = sync.NewCond(&mutex)
	mail := 1
	go func() {
		for count := 0; count <= 15; count++ {
			time.Sleep(time.Second)
			mail = count
			cond.Broadcast()
		}
	}()
	// worker1
	go func() {
		for mail != 5 { // 触发的条件,如果不等于5,就会进入cond.Wait()等待,此时cond.Broadcast()通知进来的时候,wait阻塞解除,进入下一个循环,此时发现mail != 5,跳出循环,开始工作。
			cond.L.Lock()
			cond.Wait()
			time.Sleep(5 * time.Second)
			fmt.Println("持有锁 worker1")
			fmt.Println(mail)
			cond.L.Unlock()
		}
		fmt.Println("worker1 started to work")
		time.Sleep(3 * time.Second)
		fmt.Println("worker1 work end")
	}()
	// worker2
	go func() {
		for mail != 10 {
			cond.L.Lock()
			cond.Wait()
			time.Sleep(5 * time.Second)
			fmt.Println("持有锁 worker2")
			fmt.Println(mail)
			cond.L.Unlock()
		}
		fmt.Println("worker2 started to work")
		time.Sleep(3 * time.Second)
		fmt.Println("worker2 work end")
	}()
	// worker3
	go func() {
		for mail != 10 {
			cond.L.Lock()
			cond.Wait()
			time.Sleep(5 * time.Second)
			fmt.Println("持有锁 worker3")
			//验证了底层挂锁的逻辑,最后几次执行,打印出来的mail都是15
			fmt.Println(mail)
			cond.L.Unlock()
		}
		fmt.Println("worker3 started to work")
		time.Sleep(3 * time.Second)
		fmt.Println("worker3 work end")
	}()
	select {}
}