前面我们学习了进程和线程,它们的调度由系统决定,接下来了解协程(Coroutine)。
我们也知道Python中的多线程是抢占式调度,在线程切换执行的过程中,每个线程执行的代码数量和时间都是由系统决定,编程人员无法控制。
如果是CPU密集型的程序,涉及大量计算,要求计算速度,多线程切换的耗时(需要保存当前线程状态,下次执行时再取回状态)带来的开销,反而不如单线程执行速度更快。
而I/O密集型程序选择多线程会比较高效,因为I/O密集型程序(例如Web应用)往往需要等待I/O操作完成,这时线程切换的耗时不会带来什么影响(某一线程等待,就跳转到另一线程执行),反而任务越多越显得高效。
但是,多线程也有很明显的缺点,就是多个线程执行的程序之间需要协作的话,会非常麻烦。大家可以回想一下多线程对同一变量的操作。
一个线程实际上就是执行一个子程序,也就是执行一个函数。
如果不好理解,回想一下创建多线程的语句,Thread的参数target就是一个函数名称。
那么,既然知道线程之间子程序的协作有很明显的弊端,怎么解决它呢?
这里就可以用到我们这篇教程的学习目标–协程。
如果想理解协程,建议记住以下几点:
协程是在单线程中子程序之间的协同工作,没有切换线程的开销,效率高于多线程。
协程中不存在同时写入变量的冲突,控制共享资源无需加锁。
协程不是函数调用。(这点很重要,下面会详细解释。)
协程需要使用生成器来实现。
协程可以通过异步I/O实现多任务并发。
首先,我们先来区分协程和函数调用。
先来看一个例子。
首先,函数run()通过循环得到数字;
然后,函数fun()负责将产生的数字加1;
最后,再由函数run()显示输出计算后的结果。
示例代码:(函数调用)
import time
def fun(i):
print('接到任务...')
time.sleep(1)
i += 1
print('完成任务...')
return i
def run():
for i in range(3):
num = fun(i) # 调用函数
print('当前数字:', num)
if __name__ == '__main__':
run()
示例代码:(协程)
import time
def fun():
i = 0
while True:
value = yield i
if value is not None:
print('接到任务...')
time.sleep(1)
i = value + 1
print('完成任务...')
def run(cor):
next(cor)
for i in range(3):
num = cor.send(i) # 执行其它子程序
print('当前数字:', num)
cor.close()
if __name__ == '__main__':
cor = fun()
run(cor)
大家对比一下两段代码,就能够看出区别。
调用函数:是在函数run()的执行过程中调用了函数fun(),当函数fun()运行结束后,又回到函数run()继续执行。所以,两个函数并没有同时运行,只是调用没有协作。
协程:是先挂起(启动并等待)生成器fun(),然后执行run()函数,在函数run()的执行过程中,通过send()方法执行一次生成器的计算代码,再继续执行。在这个过程中,函数fun()和函数run()同时都在运行,形成了协作关系。
通过理解协程的工作过程,大家能够看到协程中什么时候执行哪一个子程序的代码以及执行哪些代码都是可以由编程人员控制的。
所以,协程的是一种用户态的轻量级线程。
提示:这个概念很硬,可以延伸了解用户态、内核态、系统空间和用户空间这些名词的含义。
有了生成器才有了协程,因为生成器能够挂起,所以就有了多个子程序同时运行的可能。
而且,因为生成器的send()方法,能够将数据发送到生成器,并且让生成器的执行指定的代码,才实现了子程序之间的协作。
后来,随着Python版本的更新,又出现了yield from表达式(Python3.3)。
yield from表达式的作用,官方文档是这么描述的:
允许生成器将其部分操作委托给另一个生成器。也就是允许一段包含yield的代码被分解出来放到另一个生成器中。此外,子生成器允许返回一个值,该值可用于委托生成器。
从这段描述可以看出,通过yield from表达式,我们可以获取一个生成器的部分操和它的返回值,并在此基础上进行重构。
来看一段获取一定数量奇数的示例代码。
示例代码:
def odds(max): # 定义子生成器
odd = 1 # 起始奇数
count = 0 # 获取次数
while odd <= max: # 循环获取到最大奇数值
value = yield odd # 生成奇数
odd += 2 # 计算下一个奇数
count += 1 # 生成次数递增
if value is None or True: # 获取外部传入的值
print('第', count, '生成成功...')
return count # 返回生成次数
def copy_odds(max): # 定义委托生成器
print('-' * 8, '开始', '-' * 8)
count = yield from odds(max) # 获取生成器部分操作以及返回值
print('-' * 8, '完成', '-' * 8)
print('共生成了', count, '个奇数。')
if __name__ == '__main__':
gen = copy_odds(9) # 创建生成器对象
print(gen.send(None)) # 挂起生成器
while True:
try:
print(gen.send(True)) # 运行一次生成器并将值传入
except: # 迭代结束跳出循环
break
通过上方代码,我们可以更直观的看出yield from表达式的作用:
可以获取子生成器的部分操作创建委托生成器;
可以获取子生成器的返回值;
可以将send信息传递给子生成器,并且处理好了各种异常情况。
接下来,我们来了解协程结合异步I/O实现多任务并发。
asyncio是一个基于事件循环实现异步I/O的模块。
提示:事件循环可以简单的理解为等待程序分配事件并进行相应的处理,它能同时处理多个事件。
如何使用asyncio模块,我们先来看一段代码。
示例代码:
import asyncio
@asyncio.coroutine # 装饰函数为协程
def fun(i): # 定义协程函数
while True:
print('接到任务...')
yield from asyncio.sleep(1) # 挂起当前协程,获得内层协程的Future对象,并执行内层协程到完成。
i += 1
print('完成任务...')
return i
@asyncio.coroutine # 装饰函数为协程
def run(): # 定义协程函数
for i in range(3):
num = yield from fun(i) # 挂起当前协程,获得内层协程的Future对象,并执行内层协程到完成。
print('当前数字:', num)
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 创建循环事件
tasks = [] # 创建任务列表
for i in range(2):
tasks.append(run()) # 添加任务到任务列表
loop.run_until_complete(asyncio.wait(tasks)) # 循环运行直到完成(等待任务列表中的协程和Future对象执行完成)
loop.close() # 关闭循环事件
上方代码和我们第一段协程示例代码的功能相同。
这里有一些关键内容需要理解(方法与函数仅列出部分主要参数)。
@asyncio.coroutine装饰器:根据官方文档的描述,基于生成器的协程函数应该使用@asyncio.coroutine进行装饰,虽然并不需要严格执行。所以,你会发现,即便没有装饰程序依然正常。
asyncio.sleep(delay):创建一个协程并在指定时间完成,参数delay为秒数。
asyncio.get_event_loop():获取当前上下文的事件循环对象。
asyncio.wait(futures):等待指定Future对象序列中的Future对象和协程对象执行完成。协程将被包含到任务(Task)中,返回两种Future对象(完成的和即将执行的)。
Task:安排一个协程的执行:把它包含到一个Future对象中。Task是Future的一个子类。一个任务对象(Task)是负责在事件循环执行一个协程对象。
run_until_complete:运行所有的Future对象直到完成。
接下来,我们来看一下这段代码的执行过程。
创建了事件循环对象;
创建了任务列表;
运行任务列表中的任务,直到所有任务结束。
关闭事件循环。
整个过程中的第3步,其实非常复杂,我们分解来看。
注意:任务列表中是2个任务。
首先,事件循环会同时执行任务1(tasks[0])和任务2(tasks[1]);
然后,以执行任务1为例。
先执行任务1的协程run(),当执行到yield from表达式,开始执行协程fun(),此时会返回一个Future对象(假设叫F1)传递给事件循环,事件循环开始一直监视F1,并暂停协程run();
执行协程fun()的过程中又遇到yield from表达式,开始执行协程asyncio.sleep(1),这时也会返回一个 Future对象(假设叫F2)并将其传递给事件循环,事件循环开始一直监控这个F2(同时也在监视F1),同时暂停协程fun()的执行。
1秒钟之后,协程asyncio.sleep()执行结束,事件循环会选择F2以及被暂停协程fun(),将F2的结果返回给协程fun(),然后协程fun()继续执行到结束。
协程fun()执行结束后,事件循环选择F1以及被暂停协程run(),将F1的结果返回给协程run(),然后将协程run()继续执行。
因为协程run()会循环执行3次yield from表达式,所以上面的过程也会重复执行3次。
通过yield from表达式,我们可以将协程的控制权交给事件循环,然后挂起当前协程,进入内层协程;并且,由事件循环决定何时返回当前协程接着向后执行代码。
以上就是通过协程和异步I/O实现多任务并发。
不过,Python 3.5版本中,出现了新的语法async和await。
通过async语法定义的函数即为协程,无需通过@asyncio.coroutine装饰器进行装饰。
通过await语法等待内层协程的执行完成。
所以,之前协程函数的代码可以使用新语法来编写。
示例代码:
async def fun(i): # 定义协程函数
while True:
print('接到任务...')
await asyncio.sleep(1) # 等待内层协程执行完成
i += 1
print('完成任务...')
return i
async def run(): # 定义协程函数
for i in range(3):
num = await fun(i) # 等待内层协程执行完成
print('当前数字:', num)
如果不需要兼容旧版本的Python,官方文档中建议使用新的语法。