此文紧接上文协程和并发课程【部分三~四】。
本文的难度会比较大,主要是第七部分,用协程来写一个操作系统。
第五部分:协程与任务(Task)
注:这里的任务与操作系统的任务概念一致。
对于任务,它有几个核心特性:
- 独立控制流
- 内部状态
- 可以调度(挂起和恢复)
- 能够与其他任务通信
作者认为协程看起来就像任务(Coroutines look like tasks)
。为此,作者针对任务的这些特性一一提出论证:
协程有自己的控制流
协程有自己的内部状态(变量),而且内部变量的生存时间跟协程一样,它们构成了执行环境,如
协程之间可以通信,seng() 方法可以向协程发送数据,而 yield 表达式可以接受输入
协程可以挂起和恢复:yield 挂起执行,send() 恢复执行,close() 结束执行。
不过有一点,协程不像任务一样可以跟线程和子进程绑定。
第六部分:操作系统
程序执行
对于一个 CPU 而言,程序是一系列的指令(如下图);而且对于它而言,每刻最多只做一件事,也不知道任务切换的问题。
多任务问题
既然 CPU 不知道多任务的问题,应用程序也不可能知道,那只有可能是操作系统知道了。那操作系统是如何做到多任务的呢?答案就是中断(Interrupt,硬件)
和陷阱(Trap,软件)
。在这两种情形中,CPU 都会暂时挂起正在做的事情,而去执行操作系统的“代码”,也就是在这个时候,操作系统可能会进行任务切换。
陷阱和系统调用
底层的系统调用实际都是陷阱,它是一条特殊的指令:
每当陷阱发生,应用程序都会被挂起,操作系统接过控制权:
在陷阱发生期间,操作系统进行任务切换:
当任务比较多的时候,还需要进行调度,以确定下一个要运行的任务:
而对于 yield 而言,它就像是一种“陷阱”,当生成器函数执行到 yield 的时候会被挂起,然后控制权转移回调用者。如果我们把 yield 当做陷阱,那我们就可以用 Python 建立一个多任务的操作系统。
第七部分:创建操作系统
我们要创建的操作系统:
- 支持多任务
- 使用纯 Python 代码
- 没有线程
- 没有子进程
- 使用生成器/协程
第 1 步:定义任务
1 | # ------------------------------------------------------------ |
第 2 步:定义调度器
1 | # ------------------------------------------------------------ |
第 3 步:定义任务出口(Exit)
1 | # ------------------------------------------------------------ |
第 4 步:系统调用
1 | # ------------------------------------------------------------ |
第 5 步:任务管理
对于任务,它:
- 不能看到调度器
- 不能看见其他任务
- yield 是唯一的对外接口(在任务内)
一些常见的任务管理函数:
- 创建新任务
- 杀死任务
- 等待任务
先来看下包括创建和杀死任务的样例:
1 | # ------------------------------------------------------------ |
添加等待子进程退出任务管理:
1 | # ------------------------------------------------------------ |
回显服务器
很神奇吧,我们还能基于刚新建的操作系统来创建一个回显服务器(Echo Server)。Generator and Coroutine Rock!!!
这个回显服务器是阻塞的,但没关系,测试嘛:
1 | # echobad.py |
作者没有提供客户端,那我们就自己写一个咯:
1 | # Echo client |
测试结果如下:
第 6 步:I/O 等待
因为上边的服务器是阻塞(socket.accept)的,这样整个 Python 解释器都会被阻塞掉,所以我们要继续改进操作系统为真正的多任务系统,让其支持 I/O 等待。
改进操作系统
1 | # ------------------------------------------------------------ |
改进回显服务器
1 | # echogood2.py |
测试效果如下:
总结
到目前为止,我们已经,
- 创建了一个多任务操作系统
- 支持并行运行任务
- 支持创建、销毁、等待任务
- 支持任务的 I/O 操作
- 甚至可以写一个并发服务器