从生成器到协程
从Python2到Python3,协程经历了翻天覆地的变化。
简单的生成器
https://www.python.org/dev/peps/pep-0255/
Python2.3中,加入了新的关键字yield
在PEP255中,引入了yield表达式
规定yield语句只能在函数中使用。包含yield语句的函数被称为生成器函数。
当执行到yield语句是,函数的状态会被冻结(挂起所有状态,如:局部变量、指令指针、堆栈状态等),以便下载调用next时恢复状态继续执行。
通过生成器实现协程
协程的底层架构是在 PEP 342 中定义,在 Python2.5 实现的。
实现思想:使用yield挂起生成器,使用send 方法激活生成器。这样生成器就具备实现协程的功能。
send方法类似于next,不同的是send方法可以修改值,next不能修改只能取下一个。
执行 generator.send(None) 完全等同于调用生成器的 next 方法。使用其它参数调用 send 也有同样的效果,不同的是,当前生成器表达式产生的值会不一样。
参考资料:PEP 342 Coroutines via Enhanced Generators
协程的演变
Python2 中生成器中 return 语句提供值,会抛出 SyntaxError 异常。
Python3 中生成器可以 return 一个值,
Python3.3 增加了 yield from 语法,使调用嵌套生成器变得简单
Python3.5 加入了关键字 async 和 await ,将生成器和协程彻底分开。
参考资料:
PEP 380 Syntax for Delegating to a Subgenerator
PEP 525 Asynchronous Generators
生成器原理
函数是如何执行的
Python 解释器是基于栈的,其中有三种栈:调用栈 (frame stack)、数据栈 (data stack)、块栈 (block statck)。
对于Python编译器来说,PyCodeObject对象是其真正的编译结果。
在代码运行时,字节码会存储在 PyCodeObject 对象中。PyCodeObject 保存的是编译后的静态信息,在运行的时候会结合上下文形成一个完整的运行态环境。函数的__code__
变量其实就是函数的编译后的PyCodeObject对象
生成器是如何执行的
YIELD_VALUE 指令,挂起当前函数
调用生成器函数时,生成器不会立即执行,而是返回一个生成器对象。生成器对象有 gi_code 和 gi_frame 两个常用的属性,gi_code 和 code 一样存放了字节码,gi_frame 中存储了运行时的数据。
使用 send 方法也可以使生成器继续执行,和 next 不同的是,send 方法可以向生
成器传入值
yield 之后和之前的代码执行是分离的,函数在遇到 yield 会交出控制权,send
方法会重新获取控制权。
使用生成器实现多任务
原理:函数遇到yield挂起,就会去执行别的任务。
缺点:虽然实现了并发,但是不能检测IO阻塞(例如sleep)。
import time
def calc_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'执行时间是:{end_time - start_time}')
return result
return wrapper
def work1():
for i in range(5):
print(f'听音乐....{i}')
time.sleep(1)
yield
def work2():
for i in range(5):
time.sleep(1)
print(f'打游戏....{i}')
yield
@calc_time
def main():
g1 = work1()
g2 = work2()
while True:
try:
next(g1)
next(g2)
except StopIteration: # 没有下一个next取不到数据的时候报这个异常
break
main()
gevent实现异步
gevent 是应用非常广泛的异步网络库,底层使用 greenlet 协程
gevent能自动检测io阻塞,遇到阻塞自动切换任务。
import time
import gevent
from gevent import monkey # gevent的补丁,为了解决gevent遇到time.sleep不切换的问题
monkey.patch_all() # 使用gevent的补丁
def calc_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'执行时间是:{end_time - start_time}')
return result
return wrapper
def work1():
for i in range(5):
print(f'听音乐....{i}')
# time.sleep(1)
gevent.sleep(60*5)
def work2():
for i in range(5):
time.sleep(1) # gevent能自动检测io阻塞,遇到阻塞自动切换任务
print(f'打游戏....{i}')
@calc_time
def main():
g1 = gevent.spawn(work1) # 创建协程1
g2 = gevent.spawn(work2) # 创建协程2
g1.join()
g2.join()
if __name__ == '__main__':
main()
协程非常安全,遇到阻塞自动切换。
协程核心思想
为了实现异步IO。
协程是为了保证线程安全设计的。
若干个协程任务,当某个任务遇到阻塞时,自动切换到非阻塞的任务上。
阻塞: IO阻塞。 如:
- input(“请输入一个数:”)
- 磁盘IO(写数据是需要时间的)
- 网络IO(网络请求数据是需要时间的)
- time.sleep(2)
asyncio实现异步
import time
import asyncio
def calc_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'执行时间是:{end_time - start_time}')
return result
return wrapper
async def work1(): # 每一个方法钱都要加async标识
for i in range(5):
await asyncio.sleep(1) # 需要耗时的任务前加await,自动切换到其他任务
print(f'听音乐....{i}')
async def work2():
for i in range(5):
await asyncio.sleep(1)
print(f'打游戏....{i}')
async def main():
task1 = asyncio.create_task(work1()) # 创建协程任务
task2 = asyncio.create_task(work2())
await task1 # 启动协程
await task2
if __name__ == '__main__':
asyncio.run(main())
用户态 : 切换不需要CPU调度
核心态: 线程切换,进程切换,核心态 , 切换需要CPU调度