Go语言核心36讲(Go语言实战与应用六)--学习笔记

2021年11月22日 阅读数:1
这篇文章主要向大家介绍Go语言核心36讲(Go语言实战与应用六)--学习笔记,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

28 | 条件变量sync.Cond (下)

问题 1:条件变量的Wait方法作了什么?

在了解了条件变量的使用方式以后,你可能会有这么几个疑问。git

一、为何先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?github

二、为何要用for语句来包裹调用其Wait方法的表达式,用if语句不行吗?面试

这些问题我在面试的时候也常常问。你须要对这个Wait方法的内部机制有所了解才能回答上来。函数

条件变量的Wait方法主要作了四件事。工具

一、把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。ui

二、解锁当前的条件变量基于的那个互斥锁。操作系统

三、让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。设计

四、若是通知到来而且决定唤醒这个 goroutine,那么就在唤醒它以后从新锁定当前条件变量基于的互斥锁。自此以后,当前的 goroutine 就会继续执行后面的代码了。code

你如今知道我刚刚说的第一个疑问的答案了吗?blog

由于条件变量的Wait方法在阻塞当前的 goroutine 以前,会解锁它基于的互斥锁,因此在调用该Wait方法以前,咱们必须先锁定那个互斥锁,不然在调用这个Wait方法时,就会引起一个不可恢复的 panic。

为何条件变量的Wait方法要这么作呢?你能够想象一下,若是Wait方法在互斥锁已经锁定的状况下,阻塞了当前的 goroutine,那么又由谁来解锁呢?别的 goroutine 吗?

先不说这违背了互斥锁的重要使用原则,即:成对的锁定和解锁,就算别的 goroutine 能够来解锁,那万一解锁重复了怎么办?由此引起的 panic 但是没法恢复的。

若是当前的 goroutine 没法解锁,别的 goroutine 也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?只要共享资源的状态不变,即便当前的 goroutine 因收到通知而被唤醒,也依然会再次执行这个Wait方法,并再次被阻塞。

因此说,若是条件变量的Wait方法不先解锁互斥锁的话,那么就只会形成两种后果:不是当前的程序因 panic 而崩溃,就是相关的 goroutine 全面阻塞。

再解释第二个疑问。很显然,if语句只会对共享资源的状态检查一次,而for语句却能够作屡次检查,直到这个状态改变为止。那为何要作屡次检查呢?

这主要是为了保险起见。若是一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。

这种状况是颇有可能发生的,具体以下面所示。有多个 goroutine 在等待共享资源的同一种状态。好比

一、有多个 goroutine 在等待共享资源的同一种状态。好比,它们都在等mailbox变量的值不为0的时候再把它的值变为0,这就至关于有多我的在等着我向信箱里放置情报。虽然等待的 goroutine 有多个,但每次成功的 goroutine 却只可能有一个。别忘了,条件变量的Wait方法会在当前的 goroutine 醒来后先从新锁定那个互斥锁。在成功的 goroutine 最终解锁互斥锁以后,其余的 goroutine 会前后进入临界区,但它们会发现共享资源的状态依然不是它们想要的。这个时候,for循环就颇有必要了。

二、共享资源可能有的状态不是两个,而是更多。好比,mailbox变量的可能值不仅有0和1,还有二、三、4。这种状况下,因为状态在每次改变后的结果只可能有一个,因此,在设计合理的前提下,单一的结果必定不可能知足全部 goroutine 的条件。那些未被知足的 goroutine 显然还须要继续等待和检查。

三、有一种可能,共享资源的状态只有两个,而且每种状态都只有一个 goroutine 在关注,就像咱们在主问题当中实现的那个例子那样。不过,即便是这样,使用for语句仍然是有必要的。缘由是,在一些多 CPU 核心的计算机系统中,即便没有收到条件变量的通知,调用其Wait方法的 goroutine 也是有可能被唤醒的。这是由计算机硬件层面决定的,即便是操做系统(好比 Linux)自己提供的条件变量也会如此。

综上所述,在包裹条件变量的Wait方法的时候,咱们老是应该使用for语句。

好了,到这里,关于条件变量的Wait方法,我想你知道的应该已经足够多了。

问题 2:条件变量的Signal方法和Broadcast方法有哪些异同?

条件变量的Signal方法和Broadcast方法都是被用来发送通知的,不一样的是,前者的通知只会唤醒一个所以而等待的 goroutine,然后者的通知却会唤醒全部为此等待的 goroutine。

条件变量的Wait方法总会把当前的 goroutine 添加到通知队列的队尾,而它的Signal方法总会从通知队列的队首开始,查找可被唤醒的 goroutine。因此,因Signal方法的通知,而被唤醒的 goroutine 通常都是最先等待的那一个。

这两个方法的行为决定了它们的适用场景。若是你肯定只有一个 goroutine 在等待通知,或者只需唤醒任意一个 goroutine 就能够知足要求,那么使用条件变量的Signal方法就行了。

不然,使用Broadcast方法总没错,只要你设置好各个 goroutine 所指望的共享资源状态就能够了。

此外,再次强调一下,与Wait方法不一样,条件变量的Signal方法和Broadcast方法并不须要在互斥锁的保护下执行。偏偏相反,咱们最好在解锁条件变量基于的那个互斥锁以后,再去调用它的这两个方法。这更有利于程序的运行效率。

最后,请注意,条件变量的通知具备即时性。也就是说,若是发送通知的时候没有 goroutine 为此等待,那么该通知就会被直接丢弃。在这以后才开始等待的 goroutine 只可能被后面的通知唤醒。

你能够打开 demo62.go 文件,并仔细观察它与 demo61.go 的不一样。尤为是lock变量的类型,以及发送通知的方式。

package main

import (
	"log"
	"sync"
	"time"
)

func main() {
	// mailbox 表明信箱。
	// 0表明信箱是空的,1表明信箱是满的。
	var mailbox uint8
	// lock 表明信箱上的锁。
	var lock sync.Mutex
	// sendCond 表明专用于发信的条件变量。
	sendCond := sync.NewCond(&lock)
	// recvCond 表明专用于收信的条件变量。
	recvCond := sync.NewCond(&lock)

	// send 表明用于发信的函数。
	send := func(id, index int) {
		lock.Lock()
		for mailbox == 1 {
			sendCond.Wait()
		}
		log.Printf("sender [%d-%d]: the mailbox is empty.",
			id, index)
		mailbox = 1
		log.Printf("sender [%d-%d]: the letter has been sent.",
			id, index)
		lock.Unlock()
		recvCond.Broadcast()
	}

	// recv 表明用于收信的函数。
	recv := func(id, index int) {
		lock.Lock()
		for mailbox == 0 {
			recvCond.Wait()
		}
		log.Printf("receiver [%d-%d]: the mailbox is full.",
			id, index)
		mailbox = 0
		log.Printf("receiver [%d-%d]: the letter has been received.",
			id, index)
		lock.Unlock()
		sendCond.Signal() // 肯定只会有一个发信的goroutine。
	}

	// sign 用于传递演示完成的信号。
	sign := make(chan struct{}, 3)
	max := 6
	go func(id, max int) { // 用于发信。
		defer func() {
			sign <- struct{}{}
		}()
		for i := 1; i <= max; i++ {
			time.Sleep(time.Millisecond * 500)
			send(id, i)
		}
	}(0, max)
	go func(id, max int) { // 用于收信。
		defer func() {
			sign <- struct{}{}
		}()
		for j := 1; j <= max; j++ {
			time.Sleep(time.Millisecond * 200)
			recv(id, j)
		}
	}(1, max/2)
	go func(id, max int) { // 用于收信。
		defer func() {
			sign <- struct{}{}
		}()
		for k := 1; k <= max; k++ {
			time.Sleep(time.Millisecond * 200)
			recv(id, k)
		}
	}(2, max/2)

	<-sign
	<-sign
	<-sign
}

总结

咱们今天主要讲了条件变量,它是基于互斥锁的一种同步工具。在 Go 语言中,咱们须要用sync.NewCond函数来初始化一个sync.Cond类型的条件变量。

sync.NewCond函数须要一个sync.Locker类型的参数值。

sync.Mutex类型的值以及sync.RWMutex类型的值均可以知足这个要求。另外,后者的RLocker方法能够返回这个值中的读锁,也一样能够做为sync.NewCond函数的参数值,如此就能够生成与读写锁中的读锁对应的条件变量了。

条件变量的Wait方法须要在它基于的互斥锁保护下执行,不然就会引起不可恢复的 panic。此外,咱们最好使用for语句来检查共享资源的状态,并包裹对条件变量的Wait方法的调用。

不要用if语句,由于它不能重复地执行“检查状态 - 等待通知 - 被唤醒”的这个流程。重复执行这个流程的缘由是,一个“由于等待通知,而被阻塞”的 goroutine,可能会在共享资源的状态不知足其要求的状况下被唤醒。

条件变量的Signal方法只会唤醒一个因等待通知而被阻塞的 goroutine,而它的Broadcast方法却能够唤醒全部为此而等待的 goroutine。后者比前者的适应场景要多得多。

这两个方法并不须要受到互斥锁的保护,咱们也最好不要在解锁互斥锁以前调用它们。还有,条件变量的通知具备即时性。当通知被发送的时候,若是没有任何 goroutine 须要被唤醒,那么该通知就会当即失效。

思考题

sync.Cond类型中的公开字段L是作什么用的?咱们能够在使用条件变量的过程当中改变这个字段的值吗?

笔记源码

https://github.com/MingsonZheng/go-core-demo

知识共享许可协议

本做品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、从新发布,但务必保留文章署名 郑子铭 (包含连接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的做品务必以相同的许可发布。