介绍一下goroutine并发控制与通信的相关内容。

背景

一般来说,多个goroutine之间经常是需要同步与通信的。
目前实现多个goroutine间同步与通信的方式(控制并发的方式)大致有:

1. 全局共享变量
2. channel通信(CSP模型)
3. Context包

goroutine退出机制

  1. 当前goroutine的退出机制设计是:goroutine退出只能由本身控制,不允许从外部强制结束。
  2. 只有两种情况例外,那就是main函数结束或者程序崩溃结束运行。
  3. 所以要实现主进程控制子goroutine的开始和结束,必须借助其它工具来实现。

全局共享变量

此方式最简单,其实现步骤是:

1. 声明一个全局变量。
2. 所有子goroutine共享这个变量,并不断轮询这个变量检查是否有更新。
3. 在主进程中更新此全局变量。
4. 子goroutine检测到全局变量更新,然后执行相应的逻辑。

此方式的特点:

1. 优点:简单方便,通过一个变量就可以控制所有子 goroutine 的开始和结束。
2. 缺点:功能有限,不适合用于子 goroutine 间的通信,因为全局变量可以传递的信息很小。
3. 缺点:主进程无法等待所有子 goroutine 退出,因为这种方式只能是单向通知,只适用于非常简单的逻辑且并发量不太大的场景。

协程池

  1. 在日常大部分场景下,不需要使用协程池。因为Goroutine非常轻量,默认2kb,使用go func()很难成为性能瓶颈。当然一些极端情况下需要追求性能,可以使用协程池实现资源的复用,例如FastHttp使用协程池性能提高许多。

channel

channel的线程安全

  1. Channel是Go中的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通信。
  2. Channel也可以理解成是一个先进先出的队列,通过管道进行通信。
  3. Go中发送一个数据到Channel和从Channel接收一个数据都是原子性的。
  4. Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存。前者就是传统的加锁,后者就是Channel。
  5. 设计Channel的主要目的就是在多任务间传递数据的,当然要保证安全。

CSP

  1. CSP 是 Communicating Sequential Process 的简称,中文叫做通信顺序进程,是一种并发编程模型。
  2. CSP 模型的关键是关注 channel,而不关注发送消息的实体。
  3. Go 语言实现了 CSP 部分理论,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。
  4. CSP 描述这样一种并发模型:多个 Process 使用一个 Channel 进行通信, 这个 Channel 连结的 Process 通常是匿名的,消息传递通常是同步的(有别于 Actor Model)。

waitgroup

借助标准库 sync 里的 Waitgroup,可以实现优雅等待所有子 goroutine 完全结束之后主进程才结束退出。这是一种控制并发的方式,可以实现对多 goroutine 的等待。

1. 创建一个 Waitgroup 的实例 wg。
2. 在每个 goroutine 启动的时候,调用 wg.Add(1) 进行注册。
3. 在每个 goroutine 完成任务后退出之前,调用 wg.Done() 进行注销。
4. 在等待所有 goroutine 的地方调用 wg.Wait() 阻塞进程,直到所有 goroutine 都完成任务调用,然后Wait()方法会返回。

Context

Context上下文

1. 每个 Goroutine 在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态封装在一个 Context 变量中,传递给要执行的 Goroutine。
2. 上下文几乎已经成为传递与请求同生存周期变量的标准方法。
3. 在网络编程下,当接收到一个网络请求 Request,在处理这个 Request 的 goroutine 中,可能需要在当前 gorutine 继续开启多个新的 Goroutine 来获取数据与逻辑处理(例如访问数据库、RPC 服务等)。即一个请求 Request,会需要多个 Goroutine 进行处理,这些 Goroutine 可能需要共享 Request 的一些信息。同时当 Request 被取消或者超时的时候,所有从这个 Request 创建的所有 Goroutine 也应该被结束。

Context的链式调用

可以使用 context.Background 方法来生成Context根节点,而后可以进行链式调用使用 context 包里的各类方法:

func Background() Context
func TODO() Context

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

Context使用规范

  1. 不要把 Context 放入一个结构体当中,而应该显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为 ctx。
  2. 即使方法允许,也不要传入一个 nil 的 Context,如果你不确定你要用什么 Context 的时候传一个 context.TODO()。
  3. 使用 context 的 Value 相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数。
  4. 同样的 Context 可以用来传递到不同的 goroutine 中,Context 在多个 goroutine 中是并发安全的。
  5. 在context包内部已经实现好了两个空的Context,可以通过调用Background()和TODO()方法获取。一般是将它们作为Context的根,往下派生。

并发编程模型

在并发编程的模型选择上,有两个流派,一个是共享内存模型,一个是消息传递模型。

并发控制

控制并发有三种经典的方式:

  1. 通过channel通知进行并发控制
  2. 通过WaitGroup进行并发控制
  3. 通过Context进行并发控制