协程

协程(Coroutines)是一种比线程更加轻量级的存在,是用户级别的线程,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程

协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源,而由于协程完全是在用户态执行,因此,协程间的调度也是由用户来进行

我们都知道,线程在进行切换的时候,会保存线程的上下文,然后将其挂起,协程也是如此,用户可以使用系统提供的API来保存协程的上下文然后进行切换,例如Linux的ucontext

拥有了切换的基础—上下文的保存,我们就可以设计调度的时机了,首先考虑一个线程中包含多个协程的情况,这个情况,就是远古时期中单核CPU+多线程的情况,其实是没有并发与线程安全这一说法的,因为同一时间只有一个线程在运行,因此,协程也是如此,毕竟对协程来说,线程就是执行协程的CPU

系统在进行线程切换的时候,会使用很多种策略,比如按时间片公平分配给每个线程,以及当IO发生,线程挂起的时候,进行线程切换

那么协程应该怎么做呢?由于我们不需要像操作系统那样考虑那么多的情况,我们只考虑所处的业务场景,那么一切就变得简单起来

比如,我需要使用协程来构建一个echo服务器程序,那么,需要考虑的场景就是

  • 当客户端到来的时候,新建一个协程进行处理
  • 当客户端没有发数据过来的时候,挂起协程然后等待,此时进行协程的切换
  • 当客户端数据到达的时候,唤醒挂起的协程,进行切换

简单来说就是,没有数据,切出去,有数据到达,切回来,整个的流程是非常的简洁明了的,而这个场景利用传统的异步模型来做,也是非常简单的

优势

那么协程相对于传统的异步模型来说,有什么优势呢?我觉得,最大的优势是在于,协程不需要用户来手工维护上下文

考虑下面的一个场景

一个web服务器,需要解析HTTP请求然后进行回应,我们知道,HTTP是面向文本的协议,我们通过解析到的数据来确定解析状态,一个HTTP报文大概是这个样子的

1
2
3
4
HTTP line\r\n
HTTP headers\r\n
\r\n
HTTP body

很多时候,无法一次读完整个报文的所有数据,只能读一部分,解析一部分

如果使用异步模型的话,流程大概是这样的

1
2
3
4
5
6
7
读数据
解析数据
如果没有数据可读,然后也没有解析完,那么记录当前的解析状态然后结束
数据再次到达,读数据
恢复之前的解析状态
解析新数据
...

其中比较麻烦的就是保存解析状态恢复解析状态的过程,这个过程需要保存和恢复解析的上下文

但是如果用协程来做的话,流程是这样的

1
2
3
4
5
6
读数据
解析
如果没有数据可读,直接挂起
数据到达,协程被唤醒,继续读数据
解析
...

整个流程就非常连贯,也不需要用户来保存中间变量,这个对于一些需要维护n多状态的异步程序来说,是非常nice的,直接就卸掉了一堆心智负担,因为维护状态的成本是非常高的,特别是程序流不连贯的情况下,稍有不慎就会出错

多个线程下的协程

之前讨论的情况都是单线程下的协程,由于只有一个线程,因此不需要考虑过多的并发问题,除非没有设计好协程的调度

比如在协程A进入临界区之后,将协程A切换到协程B,然后协程B尝试进入临界区,就算无法进入也不会切出将调度机会让给A,导致一个死锁的状态,如果真的搞出这种骚操作,建议还是回炉重造吧XD

上面这个例子,虽然奇葩,但是也说明啦,就算在单个线程-多个协程的状态也依然会有死锁的情况,更别说多线程下的协程

因此,设计一个和协程特性结合的互斥机制非常重要

Do not communicate by sharing memory; instead, share memory by communicating.

这个利用通信来共享内存的哲学,影响了go的协程设计,那么,为什么要这样做呢?

首先,利用通信来共享内存,本质上要解决的问题就是访问临界资源的问题,其最基本的要求就是,同一时间只能有一个人使用这个资源,那么如何确定这个资源?其实方式有很多,在同一个进程中,确定一个资源的方式就是内存地址,在同一个网络中,确定一个资源(机器)的方式就是IP地址

我们可以尝试使用通信来模拟数据的共享

A和B是分布在两个线程中的协程,它们共享一份数据D

方式1:数据D只能被一个协程持有

1
2
3
首先A创造出数据D,进行了处理,然后发送给B,并销毁了D的所有引用和指针
B接受到了数据D,然后进行处理,发送,销毁引用和指针
当A再次需要数据D的时候,等待数据,然后接收数据

方式2:AB同时持有资源,然后引入仲裁者C进行资源的管理

1
2
3
引入仲裁者C,C在一开始拥有D的权限
A意图使用D的时候,需要和C通信请求使用权限,当使用完成之后,将权限交还给C
当A正在使用D的时候,B请求D的使用权限,但是此时C并没有D的使用权限,B只能等待A将权限交回

这个仲裁者的实现,可以是一个通信线程,也可以是原子变量,最关键的就是,在访问同一个资源的时候是互斥的,比较具体的例子就是用redis实现的分布式锁