1.协程
协程,也称为轻量级线程,具备以下几个特点:
·能够在单一的系统线程中模拟多个任务的并发执行;
·在一个特定的时间,只有一个任务在运行,即并非真正地并行;
·被动的任务调度方式,即任务没有主动抢占时间片的说法。当一个任务正在执行时,外部没有办法中止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出让CPU使用权;
·每个协程都有自己的堆栈和局部变量;
每个协程都包含3种运行状态:挂起、运行和停止。停止通常表示该协程已经执行完成(包括遇到问题后明确退出执行的情况),挂起则表示该协程尚未执行完成,但出让了时间片,以后有机会时会由调度器继续执行。
2.协程库(libtask)
libtask库的下载地址是:http://swtch.com/libtask/,这个库的作者使用的是非常开放的授权协议,因此可以随意修改和使用这些代码,但是必须保持该份代码所附带的版权声明。
这个libtask库实现了以下几个关键模块:
·任务及任务管理
·任务调度器
·异步IO
·channel
这个静态库直接提供了一个main()入口函数作为协程的驱动,因此库的使用者只需按该库约定的规则实现任务函数taskmain(),启动后这些任务自然会被以协程的方式创建和调度执行。taskmain()函数的声明如下:
先来看一下简单的C程序例子:
该程序从命令行得到一个整型数作为质数的查找范围,比如用户输入了100,则该程序会列出0到100之间的所有质数。
将以上代码翻译成Go语言代码,如下:
3.任务
从上面的例子可以看出,在实现了一个任务函数后,真要让这个函数加入到调度队列中,我们还需要显式调用taskcreate()函数。下面我们大致介绍一下任务的概念,以及taskcreate()到底做了哪些事情。
任务用以下的结构表达:
可以看到,每一个任务需要保存以下这几个关键数据:
·任务上下文,用于在切换任务时保持当前任务的运行环境;
·栈
·状态
·该任务所对应的业务函数
·任务的调用参数
·之前和之后的任务
下面我们再来看一下任务的创建过程:
可以看到,这个过程其实就是创建并设置了一个Task对象,然后将该对象添加到alltask列表中,接着将该Task对象的状态设置为就绪,表示该任务可以接受调度器的调度。
4.任务调度
调度器的代码实现如下:
逻辑其实很简单,就是循环执行正在等待中的任务,直到执行完所有的任务后退出。读者可能会觉得奇怪,这个函数里根本没有调用任务所对应的业务函数的代码,那么那些代码到底是怎么执行的呢?最关键的是下面这一句调用:
5.上下文切换
taskstart()函数的具体实现代码:
我们知道,在任务执行过程中发生任务切换只会因为以下原因之一:
·该任务的业务代码主要要求切换,即主动让出执行权;
·发生了IO,导致执行阻塞。
主动出让执行权通过主动调用taskyield()来完成。在下面的代码中,taskswitch()切换上下文以具体做到任务切换,taskready()函数将一个具体的设置为等待执行状态,tasksyield()则借助其他的函数完成执行权出让:
上面的代码做了这几件事情:
·将正在执行的任务放回到等待队列中,免得永远无法再切换回来;
·将该任务的状态设置为yield;
·进行任务切换
libtask库中的fd.c进行了基于轮询的异步IO封装,并在tcpproxy.c中示范了如何使用异步IO来达成自动出让执行权的效果。
当发生IO事件时,程序会先让其他处于yield状态的任务先执行,待清理掉这些可以执行的任务后,开始调用poll来监听所有处于IO阻塞状态的pollfd,一旦有某些pollfd成功读写,则将对应的任务切换为可调度状态。
6.通信机制
我们知道,channel是推荐的goroutine之间的通信方式。而实际上,“通信”这个术语并不太适用。从根本上来说,channel只是一个数据结构,可以被写入数据,也可以被读取数据。所谓的发送数据到channel,或者从channel读取数据,说白了就是对一个数据结构的操作,仅此而已。
下面我们就来看看channel的数据结构:
可以看到channel的基本组成如下:
·内存缓存,用于存放元素
·发送队列
·接受队列
从以下这个channel的创建函数可以看出,分配的内存缓存就紧跟在这个channel结构之后:
参考:
https://www.yuque.com/docs/share/8fcfa63e-0748-4bfe-99c5-063f136c6840