编程学习网 > 数据库 > Go语言同步编程:不要让吃货们做无谓的竞争
2019
12-20

Go语言同步编程:不要让吃货们做无谓的竞争

我们在此前曾经介绍过Go语言当中的mutex(锁)机制,很多时候多个Goroutine之间的同步问题靠锁就可以解决了。但是有些时候,光有锁不能很好的解决问题。比如下面的问题:

当多个吃货们都想去争抢面包时,为了避免出现共同争抢面包(共享资源)的问题,我们可以使用一把锁才确保彼此之间有先有后的去操作。然而有一种可能就是此时面包的容器内(面包店)并未有面包,因此这些吃货们即使抢到锁成功了,也不得不面对吃不到面包的尴尬境地。

站在编程人员的角度,如果我们抢到锁,吃完面包就应该释放,如果没有面包怎么办?本着占着茅坑不怎么怎么样的原则,我们其实应该把锁释放。那么接下来会发生什么呢?多个吃货们继续去争抢锁,争抢到了之后发现还是没有面包,于是无限循环下去。在这个情况下,我们发现吃货们经常要做一些无谓的竞争,但是却没有实际的效果!

其实大家也看到了,上面的问题就是大名鼎鼎的生产者与消费者问题。解决生产者和消费者的关键就是避免不必要的竞争,此时光靠互斥锁是不行的,我们需要借助条件变量。

type Cond struct {
    // 在观测或更改条件时L会冻结 L Locker // 包含隐藏或非导出字段 }
//Locker是一个interface type Locker interface { Lock() Unlock()
}

通过上述结构,我们可以看出来,条件变量内部会包含一个Locker的对象,这个Locker是一个interface,只要支持Lock与Unlock两个方法,就有资格成为Locker的一个对象。也就是说我们要使用条件变量,必须要先有一个mutex类似的锁。下面再来看看Cond的方法有哪些:

func NewCond(l Locker) *Cond func (c *Cond) Broadcast() func (c *Cond) Signal() func (c *Cond) Wait() 

方法说明:

  • NewCond 传入一把锁,构造一个条件变量
  • Wait 阻塞等待条件发生
  • Broadcast/Signal 唤醒多个/一个阻塞在该条件上的Goroutine

其中Wait函数还要再强调一下:

  1. Wait执行前,该Goroutine必须已经拥有了锁

  2. Wait执行时,会释放锁,并阻塞等待条件触发

  3. 当条件被触发时,阻塞在Wait的Goroutine被唤醒,并再次去抢锁

仔细以上介绍,我们仔细分析一下,多个吃货们来抢面包时先抢锁,假设吃货A先抢到了锁,那么他去看面包没有,就去Wait阻塞等待,此时这个锁又被释放了,那么其他吃货就又可以抢到锁了,同理吃货B抢到后面临和A一样的操作,那么吃货B最终也是阻塞等待,并且这把锁又被释放了,这样就很好的解决了大家无谓竞争的问题,当没有面包的情况下,大家都安静的等待条件的发生。当事情真的发生了,那就再继续抢锁就可以了。

下面我们使用Go语言来实现一个简单的生产者和消费者模型,我们定义一个面包筐:

var five [5]int var five_count int 

面包筐最大上限是5,这样面包生产者也不是肆无忌惮的生产,为此我们需要定义2组条件变量,一组用来控制消费者们,一组用来控制生产者的肆意生产。

var cond *sync.Cond var mutex sync.Mutex //每个条件变量都要绑定一个锁 var cond_five *sync.Cond var mutex_five sync.Mutex 

我们来实现一下生产者部分,five模拟一个环形队列:

func productor() { index := 0 prodnum := 1000 //生产的编号 for {
		//判断是否可以生产 mutex.Lock()
		time.Sleep(time.Millisecond * 200) //模拟生产消耗的时间 five[index] = prodnum              //真正的生产 fmt.Println("productor prodnum========", prodnum)
		prodnum++
		five_count++
		index = (index + 1) % 5 mutex.Unlock()
		cond.Signal() // 通知消费者 if five_count == 5 {
			//生产者需要休息一会 five_mutex.Lock()
			five_cond.Wait()
			//1. Wait 之前先要有锁,wait时释放锁,唤醒时去抢锁 five_mutex.Unlock()
		}
	} wg.Done()
}

我们再来实现消费者:

//消费者 func customer(num int) {
	for {
		mutex.Lock() //wait前要抢锁 if five_count > 0 {
			time.Sleep(time.Millisecond * 10) fmt.Printf("I am %d customer, prodnum == %d\n", num, five[customer_index]) customer_index = (customer_index + 1) % 5 five_count--
			five_cond.Signal() //通知生产者 }
		cond.Wait() mutex.Unlock() }
	wg.Done() }

完整代码如下:

//条件变量 /*
	Wait:
		1. 阻塞等待 Cond的发生(被唤醒) 2. 调用Wait之前,goroutine必须已经抢到锁 调用Wait时,释放锁,同时阻塞 当被唤醒时,再去抢锁,抢到后才能继续往下执行 */ package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup //消费者要等待的条件 var cond *sync.Cond var mutex sync.Mutex //生产者也有要等待的条件(可以继续生产,five_count < 5) var five_cond *sync.Cond var five_mutex sync.Mutex //共享资源 var five [5]int var five_count int = 0 var customer_index int = 0 func productor() {
	index := 0 prodnum := 1000 //生产的编号 for { //判断是否可以生产 mutex.Lock()
		time.Sleep(time.Millisecond * 200) //模拟生产消耗的时间 five[index] = prodnum //真正的生产 fmt.Println("productor prodnum========", prodnum)
		prodnum++
		five_count++
		index = (index + 1) % 5 mutex.Unlock()
		cond.Signal() // 通知消费者 if five_count == 5 { //生产者需要休息一会 five_mutex.Lock()
			five_cond.Wait() //1. Wait 之前先要有锁,wait时释放锁,唤醒时去抢锁 five_mutex.Unlock()
		}
	}
	wg.Done()
} //消费者 func customer(num int) { for {
		mutex.Lock() //wait前要抢锁 if five_count > 0 {
			time.Sleep(time.Millisecond * 10)
			fmt.Printf("I am %d customer, prodnum == %d\n", num, five[customer_index])
			customer_index = (customer_index + 1) % 5 five_count--
			five_cond.Signal() //通知生产者 }
		cond.Wait()
		mutex.Unlock()
	}
	wg.Done()
} func main() { //条件变量初始化 cond = sync.NewCond(&mutex)
	five_cond = sync.NewCond(&five_mutex)
	wg.Add(3) go productor() go customer(1) go customer(2)
	wg.Wait()
}


扫码二维码 获取免费视频学习资料

Python编程学习

查 看2022高级编程视频教程免费获取