Go中多协程协作之Sync.Cond

开发 后端
在Go中协程间通信的方式有多种,最常用的是channel。如果牵扯多个协程的通知,可以使用sync.Cond。

[[400826]]

1. 程序中的通信方式

GO语言中有句名言:“不要用共享内存来通信,而是使用通信来共享内存”。

编程语言中,通信方式分为进程间通信、线程间通信。

1.进程间通信,常用方式:

  • 有名管道
  • 无名管道
  • 信号
  • 共享内存
  • 消息队列
  • 信号灯集
  • socket

2.线程间通信,常用方式:

  • 信号量
  • 互斥锁
  • 条件变量

对于Go语言来说,Go程序启动之后对外是一个进程,内部包含若干协程,协程相当于用户态轻量级线程,所以协程的通信方式大多可以使用线程间通信方式来完成。

协程间通信方式,官方推荐使用channel,channel在一对一的协程之间进行数据交换与通信十分便捷。但是,一对多的广播场景中,则显得有点无力,此时就需要sync.Cond来辅助。

2. 什么是广播?

举个例子,上高中时,宿管老师每天早晨需要叫醒学生们去上课。这个时候,有两种解决方法:①一个寝室一个寝室的把学生叫醒。②在宿舍楼安装个广播,到起床时间时,在广播上叫醒学生。显然,使用广播的方式效率更高。

编程中的广播可以理解为:多个操作流程依赖于一个操作流程完成后才能进行某种动作,这个被依赖的操作流程在唤醒所有依赖者时使用的一种通知方式。

在Go语言中,则可以使用sync.Cond来实现多个协程之间的广播通知功能。

3. sync.Cond

cond是sync包下面的一种数据类型,相当于线程间通信的条件变量方式。

  1. // Cond implements a condition variable, a rendezvous point 
  2. // for goroutines waiting for or announcing the occurrence 
  3. // of an event. 
  4. // 
  5. // Each Cond has an associated Locker L (often a *Mutex or *RWMutex), 
  6. // which must be held when changing the condition and 
  7. // when calling the Wait method. 
  8. // 
  9. // A Cond must not be copied after first use. 
  10. type Cond struct { 
  11.     noCopy noCopy  // 在第一次使用后不可复制,使用go vet作为检测使用 
  12.  
  13.     // L is held while observing or changing the condition 
  14.   // 根据需求初始化不同的锁,如*Mutex 和 *RWMutex。注意是 指针类型 
  15.     L Locker 
  16.  
  17.   // 具有头尾指针的链表。存储被阻塞的协程,通知时操作该链表中的协程 
  18.     notify  notifyList 
  19.     checker copyChecker  // 复制检查,检查cond实例是否被复制 

 该数据类型提供的方法有:

  1. type Cond 
  2.  
  3. func NewCond(l Locker) *Cond 
  4. func (c *Cond) Broadcast() // 通知所有协程,广播 
  5. func (c *Cond) Signal()  // 通知一个协程 
  6. func (c *Cond) Wait()  // 阻塞等待,直到被唤醒 

 对应源码追溯

  1. // Wait atomically unlocks c.L and suspends execution 
  2. // of the calling goroutine. After later resuming execution, 
  3. // Wait locks c.L before returning. Unlike in other systems, 
  4. // Wait cannot return unless awoken by Broadcast or Signal. 
  5. // 
  6. // Because c.L is not locked when Wait first resumes, the caller 
  7. // typically cannot assume that the condition is true when 
  8. // Wait returnsInstead, the caller should Wait in a loop: 
  9. //       
  10. //      注意下面的写法是官方推荐的 
  11. //    c.L.Lock() 
  12. //    for !condition() { 
  13. //        c.Wait() 
  14. //    } 
  15. //    ... make use of condition ... 
  16. //    c.L.Unlock() 
  17. // 
  18. func (c *Cond) Wait() { 
  19.     // 检查c是否是被复制的,如果是就panic 
  20.     c.checker.check() 
  21.     // 获取等待队列的一个ticket数值,作为唤醒时的一个令牌凭证 
  22.     t := runtime_notifyListAdd(&c.notify) 
  23.     // 解锁 
  24.     c.L.Unlock() 
  25.    
  26.     // 注意,上面的ticket数值会作为阻塞携程的一个标识 
  27.     // 加入通知队列里面 
  28.     // 到这里执行gopark(),当前协程挂起,直到signal或broadcast发起通知 
  29.     runtime_notifyListWait(&c.notify, t) 
  30.    
  31.     // 被唤醒之后,先获取锁 
  32.     c.L.Lock() 
  33.  
  34. // Signal wakes one goroutine waiting on c, if there is any
  35. // 
  36. // It is allowed but not required for the caller to hold c.L 
  37. // during the call. 
  38. func (c *Cond) Signal() { 
  39.     c.checker.check() 
  40.     runtime_notifyListNotifyOne(&c.notify)  // 随机挑选一个进行通知,wait阻塞解除 
  41.  
  42. // Broadcast wakes all goroutines waiting on c. 
  43. // 
  44. // It is allowed but not required for the caller to hold c.L 
  45. // during the call. 
  46. func (c *Cond) Broadcast() { 
  47.     c.checker.check() 
  48.     // 通知所有阻塞等待的协程 
  49.     // 主要是唤醒 cond.notify 链表上的各个协程 
  50.     runtime_notifyListNotifyAll(&c.notify) 

 使用方法,代码示例:

  1. var locker sync.Mutex 
  2. var cond = sync.NewCond(&locker) 
  3.  
  4. // NewCond(l Locker)里面定义的是一个接口,拥有lock和unlock方法。 
  5. // 看到sync.Mutex的方法,func (m *Mutex) Lock(),可以看到是指针有这两个方法,所以应该传递的是指针 
  6. func main() { 
  7.     // 启动多个协程 
  8.     for i := 0; i < 10; i++ { 
  9.         gofunc(x int) { 
  10.             cond.L.Lock()          // 获取锁 
  11.             defer cond.L.Unlock()  // 释放锁 
  12.            
  13.             cond.Wait()   // 等待通知,阻塞当前 goroutine 
  14.            
  15.             // 通知到来的时候, cond.Wait()就会结束阻塞, do something. 这里仅打印 
  16.             fmt.Println(x) 
  17.         }(i) 
  18.     } 
  19.    
  20.     time.Sleep(time.Second * 1) // 睡眠 1 秒,等待所有 goroutine 进入 Wait 阻塞状态 
  21.     fmt.Println("Signal..."
  22.     cond.Signal()               // 1 秒后下发一个通知给已经获取锁的 goroutine 
  23.    
  24.     time.Sleep(time.Second * 1) 
  25.     fmt.Println("Signal..."
  26.     cond.Signal()               // 1 秒后下发下一个通知给已经获取锁的 goroutine 
  27.    
  28.     time.Sleep(time.Second * 1) 
  29.     cond.Broadcast()            // 1 秒后下发广播给所有等待的goroutine 
  30.     fmt.Println("Broadcast..."
  31.     time.Sleep(time.Second * 1) // 等待所有 goroutine 执行完毕 

 总结

在Go中协程间通信的方式有多种,最常用的是channel。如果牵扯多个协程的通知,可以使用sync.Cond。

查看channel、sync.Cond源码之后会发现,它们有相似之处:

  1. 阻塞协程统一被封装在 sudog 结构里面
  2. channel阻塞读/写时,用双向链表存储被阻塞导致等待唤醒的协程
  3. sync.Cond用带有头尾指针的单向链表存储被阻塞导致等待唤醒的协程
  4. 阻塞时都是使用gopark()进行协程的挂起操作

虽说有相似之处,但是却有本质区别:

  1. channel 可以用来在协程间传递数据
  2. sync.Cond 不可以在协程间传递数据,主要用来进行协程的阻塞唤醒操作。如果需要传递数据,则需要全局变量进行传递

 

责任编辑:姜华 来源: 今日头条
相关推荐

2023-06-26 08:28:35

Sync.CondGolang

2021-07-06 07:46:07

Go语言编程

2023-11-28 08:01:48

互斥锁共享资源

2023-07-13 08:06:05

应用协程阻塞

2021-09-27 23:28:29

Go多协程并发

2016-10-28 17:39:47

phpgolangcoroutine

2018-12-04 14:00:41

协程编程模式PHP

2023-07-27 13:46:10

go开源项目

2021-04-25 09:36:20

Go协程线程

2021-05-20 09:14:09

Kotlin协程挂起和恢复

2021-09-16 09:59:13

PythonJavaScript代码

2022-10-28 10:45:22

Go协程GoFrame

2021-08-04 16:19:55

AndroidKotin协程Coroutines

2022-09-12 06:35:00

C++协程协程状态

2023-11-17 11:36:59

协程纤程操作系统

2021-02-19 06:56:33

架构协程应用

2023-10-24 19:37:34

协程Java

2021-12-09 06:41:56

Python协程多并发

2020-02-24 10:39:55

Python函数线程池

2022-09-06 20:30:48

协程Context主线程
点赞
收藏

51CTO技术栈公众号