1. 程序中的通信形式
GO 语言中有句名言:“不要用共享内存来通信,而是应用通信来共享内存”。
编程语言中,通信形式分为过程间通信、线程间通信。
- 过程间通信,罕用形式:
- 有名管道
- 无名管道
- 信号
- 共享内存
- 音讯队列
- 信号灯集
- socket
- 线程间通信,罕用形式:
- 信号量
- 互斥锁
- 条件变量
对于 Go 语言来说,Go 程序启动之后对外是一个过程,外部蕴含若干协程,协程相当于用户态轻量级线程,所以协程的通信形式大多能够应用线程间通信形式来实现。
协程间通信形式,官网举荐应用 channel,channel 在一对一的协程之间进行数据交换与通信非常便捷。然而,一对多的播送场景中,则显得有点有力,此时就须要 sync.Cond 来辅助。
2. 什么是播送?
举个例子,上高中时,宿管老师每天晚上须要叫醒学生们去上课。有两种办法:①一个寝室一个寝室把学生叫醒 ②在宿舍楼装置播送,到起床工夫,在播送上叫醒学生。显然,应用播送的形式效率更高。
编程中的播送能够了解为:多个操作流程依赖于一个操作流程实现后能力进行某种动作,这个被依赖的操作流程在唤醒所有依赖者时应用的一种告诉形式。
在 Go 语言中,则能够应用 sync.Cond 来实现多个协程之间的播送告诉性能。
3. sync.Cond
cond 是 sync 包上面的一种数据类型,相当于线程间通信的条件变量形式。
`// Cond implements a condition variable, a rendezvous point`
`// for goroutines waiting for or announcing the occurrence`
`// of an event.`
`//`
`// Each Cond has an associated Locker L (often a *Mutex or *RWMutex),`
`// which must be held when changing the condition and`
`// when calling the Wait method.`
`//`
`// A Cond must not be copied after first use.`
`type Cond struct {`
`noCopy noCopy // 在第一次应用后不可复制, 应用 go vet 作为检测应用 `
`// L is held while observing or changing the condition`
`// 依据需要初始化不同的锁,如 *Mutex 和 *RWMutex。留神是 指针类型 `
`L Locker`
`// 具备头尾指针的链表。存储被阻塞的协程,告诉时操作该链表中的协程 `
`notify notifyList`
`checker copyChecker // 复制查看, 查看 cond 实例是否被复制 `
`}`
该数据类型提供的办法有:
`type Cond`
`func NewCond(l Locker) *Cond`
`func (c *Cond) Broadcast() // 告诉所有协程,播送 `
`func (c *Cond) Signal() // 告诉一个协程 `
`func (c *Cond) Wait() // 阻塞期待,直到被唤醒 `
对应源码追溯
`// Wait atomically unlocks c.L and suspends execution`
`// of the calling goroutine. After later resuming execution,`
`// Wait locks c.L before returning. Unlike in other systems,`
`// Wait cannot return unless awoken by Broadcast or Signal.`
`//`
`// Because c.L is not locked when Wait first resumes, the caller`
`// typically cannot assume that the condition is true when`
`// Wait returns. Instead, the caller should Wait in a loop:`
`//`
`// 留神上面的写法是官网举荐的 `
`// c.L.Lock()`
`// for !condition() {`
`// c.Wait()`
`// }`
`// ... make use of condition ...`
`// c.L.Unlock()`
`//`
`func (c *Cond) Wait() {`
`// 查看 c 是否是被复制的,如果是就 panic`
`c.checker.check()`
`// 获取期待队列的一个 ticket 数值,作为唤醒时的一个令牌凭证 `
`t := runtime_notifyListAdd(&c.notify)`
`// 解锁 `
`c.L.Unlock()`
`// 留神,下面的 ticket 数值会作为阻塞携程的一个标识 `
`// 退出告诉队列外面 `
`// 到这里执行 gopark(),以后协程挂起,直到 signal 或 broadcast 发动告诉 `
`runtime_notifyListWait(&c.notify, t)`
`// 被唤醒之后,先获取锁 `
`c.L.Lock()`
`}`
`// Signal wakes one goroutine waiting on c, if there is any.`
`//`
`// It is allowed but not required for the caller to hold c.L`
`// during the call.`
`func (c *Cond) Signal() {`
`c.checker.check()`
`runtime_notifyListNotifyOne(&c.notify) // 随机筛选一个进行告诉,wait 阻塞解除 `
`}`
`// Broadcast wakes all goroutines waiting on c.`
`//`
`// It is allowed but not required for the caller to hold c.L`
`// during the call.`
`func (c *Cond) Broadcast() {`
`c.checker.check()`
`// 告诉所有阻塞期待的协程 `
`// 次要是唤醒 cond.notify 链表上的各个协程 `
`runtime_notifyListNotifyAll(&c.notify)`
`}`
应用办法,代码示例:
`var locker sync.Mutex`
`var cond = sync.NewCond(&locker)`
`// NewCond(l Locker)外面定义的是一个接口, 领有 lock 和 unlock 办法。`
`// 看到 sync.Mutex 的办法,func (m *Mutex) Lock(), 能够看到是指针有这两个办法, 所以应该传递的是指针 `
`func main() {`
`// 启动多个协程 `
`for i := 0; i < 10; i++ {`
`gofunc(x int) {`
`cond.L.Lock() // 获取锁 `
`defer cond.L.Unlock() // 开释锁 `
`cond.Wait() // 期待告诉,阻塞以后 goroutine`
`// 告诉到来的时候, cond.Wait()就会完结阻塞, do something. 这里仅打印 `
`fmt.Println(x)`
`}(i)`
`}`
`time.Sleep(time.Second * 1) // 睡眠 1 秒,期待所有 goroutine 进入 Wait 阻塞状态 `
`fmt.Println("Signal...")`
`cond.Signal() // 1 秒后下发一个告诉给曾经获取锁的 goroutine`
`time.Sleep(time.Second * 1)`
`fmt.Println("Signal...")`
`cond.Signal() // 1 秒后下发下一个告诉给曾经获取锁的 goroutine`
`time.Sleep(time.Second * 1)`
`cond.Broadcast() // 1 秒后下发播送给所有期待的 goroutine`
`fmt.Println("Broadcast...")`
`time.Sleep(time.Second * 1) // 期待所有 goroutine 执行结束 `
`}`
总结
在 go 中协程间通信的形式有多种,最罕用的是 channel。如果牵扯多个协程的告诉,能够应用 sync.Cond。
查看 channel、sync.Cond 源码会发现,它们 有相似之处:
- 阻塞协程对立被封装在 sudog 构造外面
- channel 阻塞读 / 写时,用双向链表存储被阻塞导致期待唤醒的协程
- sync.Cond 应用带有头尾指针的单向链表存储被阻塞导致期待唤醒的协程
- 阻塞时都是应用 gopark()进行协程的挂起操作
虽说有相似之处,但却 有本质区别:
- channel 可用来在协程间传递数据
- sync.Cond 不可用来在协程间传递数据,次要用来进行协程的阻塞唤醒操作。如须要传递数据,则须要应用全局变量进行传递
– THE END –
举荐浏览
带你意识分布式系统的 CAP 实践
关注加星标,理解「编程技术之道」