协程的定义
根据维基百科的描述,协程是一种程序组件,与子例程一样,但是协程更为一般和灵活,但在实践中使用没有子例程那样广泛。
https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B
其实这个概念不容易理解,可以先忽略,先看看协程到底是干什么用的。
协程究竟是什么呢?有什么用呢?
协程其实可以理解为是“用户态”的多线程。在多线程的模型中,操作系统会根据某种调度算法不断地切换当前正在运行的线程,由于每个线程都有自己的栈,因此在切换线程的过程中需要上下文的切换,这样会导致大量的开销,如果系统中多大量的线程,那么系统的资源就会被上下文的切换大量的消耗,导致性能的下降。协程就是用来解决这个问题的(当然还有其他优点),协程运行在同一个线程上,没有上下文的切换。和线程不同,协程可以有多个入口点,可以在指定的位置挂起和回复执行(线程只有一个入口点且只返回一次)。
下面通过生产者-消费者模型来描述下协程的作用。
在传统的多线程模型中,一般会建立一个生产者线程,和一个消费者线程,请看下面的代码(伪代码)。
Queue q; // 全局变量,用于保存消息,假设Queue是线程安全的
void ProduceThread() // 生产者线程
{
i = 0;
while(true)
{
if (!q.IsFull())
{
q.push(i);
++i;
}
}
}
void ConsumeThread() // 消费者线程
{
i = 0;
while(true)
{
if (!q.IsEmpty())
{
print q.get(0); // 打印第一个元素
q.pop(0); // 从消息队列中删除
}
}
}
int main()
{
Thread t1 = new Thread(ProceThread);
Thread t2 = new Thread(ConsumeThread);
t1.start();
t2.start();
Sleep(100s);
return 0;
}
采用协程的方式,代码实现如下(伪代码):
Queue q; // 全局变量,用于保存消息,假设Queue是线程安全的
void ProduceFunc() // 生产者
{
i = 0;
while(true)
{
if (!q.IsFull())
{
q.push(i);
++i;
}
resume(ConsumeFunc); // 执行消费者函数
}
}
void ConsumeFunc() // 消费者
{
i = 0;
while(true)
{
if (!q.IsEmpty())
{
print q.get(0); // 打印第一个元素
q.pop(0); // 从消息队列中删除
}
yield; // 主动让出CPU,让主线程可以继续执行
}
}
int main()
{
ProduceFunc();
return 0;
}
第一看看上去两段代码没有区别啊?其实在多线程的模式中,有可能是生产者生产了多个,消费者才进行消费,也就是有可能出现这样的情况,队列q中生产了1,2,3个元素,但是消费这才开始打印1,这个就和多线程的调度有关了,哪个线程获得了CPU的资源就可以执行(一般来说,操作系统的这个调度是尽可能公平的,也就是生产者和消费者获得执行的几率是差不多的)。但是这个调度是有操作系统进行的。接着再看协程的实现,resume作用就是恢复上次执行的函数,yield的作用是主动让出CPU资源。在上述例子中,会先执行ProduceFunc,然后生产1放在队列q中,这个时候ProduceFunc会恢复ConsumeFunc,让它继续执行(因为ConsumeFunc 还没执行过,那么会从函数的第一行开始执行,这是第一个入口),然后ConsumeFunc会消费队列q中的1,并且通过yield让出CPU,让ProduceFunc继续执行,然后ProduceFunc再次resume的时候ConsumeFunc会从上次yield的地方继续执行(这就是第二个入口了,也就是协程会有多个入口点)。
协程实现的效果和多线程类似,所以也可以成为“用户态”的多线程,但是协程不存在上下文的切换,而协程的调度是需要开发者自己来控制的。
其实,上面协程的列子,实现的效果是生产者生成1个消息,然后就会让消费者消费1个消息,那这个不是和同步执行差不多吗?那还不如直接使用同步执行的代码?但是,如果消费者还要等待异步的消息呢?比如要像DB获取一些数据呢?这个例子其实还没真正提现出协程的作用,后续我会继续举出更能体现协程优点的例子。