Python协程
运行效率极高,协程的切换完全由程序控制,不像线程切换需要花费操作系统的开销,线程数量越多,协程的优势就越明显。
同时,在Python中,协程不需要多线程的锁机制,因为只有一个线程,也不存在变量冲突。
协程对于IO密集型任务非常适用,如果是CPU密集型任务,推荐多进程+协程的方式。对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能。
Python协程的发展时间较长:
- Python2.5 为生成器引用.send()、.throw()、.close()方法
- Python3.3 为引入yield from,可以接收返回值,可以使用yield from定义协程
- Python3.4 加入了asyncio模块
- Python3.5 增加async、await关键字,在语法层面的提供支持
- Python3.7 使用async def + await的方式定义协程
- 此后asyncio模块更加完善和稳定,对底层的API进行的封装和扩展
- Python将于3.10版本中移除以yield from的方式定义协程
示例代码如下:
1. Python2.x 实现协程
Python2.x对协程的支持是通过generator实现的。
在generator中,我们不但可以通过for
循环来迭代,还可以不断调用next()
函数获取由yield
语句返回的下一个值。
但是Python的yield
不但可以返回一个值,它还可以接收调用者发出的参数。
下面改用协程实现生产者消费者模式,生产者生产消息后,直接通过yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产, 效率极高:
def consumer(): print("consumer----------") r = '' while True: n = yield r if not n: return print('[消费者]Consuming %s...' % n) r = '200 OK' def producer(c): print("producer----------") # 启动生成器 c.send(None) n = 0 while n < 5: n = n + 1 print('[生产者]Producing %s...' % n) r = c.send(n) print('[生产者]Consumer return: %s' % r) c.close() if __name__ == '__main__': c = consumer() producer(c) 复制代码
代码流程说明:
consumer
函数是一个generator
,把一个consumer
传入produce
后:
- 首先调用
c.send(None)
启动生成器(在生成带有yield的 generator后 第一个迭代必须是__next__()
。__next__()
和send(None)
的效果是相同的); - 然后,一旦生产了东西,通过
c.send(n)
切换到consumer
执行; consumer
通过yield
拿到消息,处理,又通过yield
把结果传回;produce
拿到consumer
处理的结果,继续生产下一条消息;produce
决定不生产了,通过c.close()
关闭consumer
,整个过程结束。
运行结果:
producer---------- consumer---------- [生产者]Producing 1... [消费者]Consuming 1... [生产者]Consumer return: 200 OK [生产者]Producing 2... [消费者]Consuming 2... [生产者]Consumer return: 200 OK [生产者]Producing 3... [消费者]Consuming 3... [生产者]Consumer return: 200 OK [生产者]Producing 4... [消费者]Consuming 4... [生产者]Consumer return: 200 OK [生产者]Producing 5... [消费者]Consuming 5... [生产者]Consumer return: 200 OK 复制代码
send(msg)
与next()
的区别
send
可以传递参数给yield
表达式,这时传递的参数会作为yield
表达式的值,而yield
的参数是返回给调用者的值。
换句话说,就是send
可以强行修改上一个yield
表达式的值。
比如函数中有一个yield
赋值a = yield 5
,第一次迭代到这里会返回5,a还没有赋值。
第二次迭代时,使用send(10)
,那么就是强行修改yield 5
表达式的值为10,本来是5的,结果a = 10
。
send(msg)
与next()
都有返回值,它们的返回值是当前迭代遇到yield
时,yield
后面表达式的值,其实就是当前迭代中yield
后面的参数。
第一次调用send
时必须是send(None)
,否则会报错,之所以为None
是因为这时候还没有一个yield
表达式可以用来赋值。
2. Python3.x 实现协程
要点:
- 使用
async def
的形式定义 - 在协程中可以使用
await
关键字,注意,其后跟的是"可等待对象"(协程, 任务 和 Future) - 协程不能直接执行,需要在
asyncio.run()
中执行,也可以跟在await
后面 async
和await
这两个关键字只能在协程中使用
import asyncio async def foo(name): await asyncio.sleep(1) # 这是一个不会阻塞的sleep,是一个协程 print(f"name = {name}") async def main(): # 协程本身就是一个可等待对象 await foo("lczmx") # 执行协程 print("done") if __name__ == '__main__': # 使用asyncio.run运行 asyncio.run(main()) 复制代码
运行结果:
name = lczmx done 复制代码
其中,
asyncio.run(main, *, debug=False)
方法就是对run_until_complete
进行了封装:loop = events.new_event_loop()
return loop.run_until_complete(main)
关于可等待对象说明
可等待对象(awaitable)是能在 await 表达式中使用的对象。可以是协程或是具有__await__()
方法的对象。
那么协程是如何成为可等待对象的呢?
collections.abc.Awaitable
类,这是为可等待对象提供的类,可被用于 await 表达式中。
class Awaitable(metaclass=ABCMeta): __slots__ = () @abstractmethod def __await__(self): # __await__方法必须返回一个 iterator yield @classmethod def __subclasshook__(cls, C): if cls is Awaitable: return _check_methods(C, "__await__") return NotImplemented 复制代码
- 用
async def
复合语句创建的函数,它返回的是一个Coroutine对象
,而Coroutine
继承Awaitable
。
3. Python3.x 使用协程进行并发操作
使用协程进行并发操作,在Python3.7以上的版本,使用asyncio.create_task(coro)
方法,返回一个Task对象,Task类继承Future,在Python3.7以下版本中,使用asyncio.ensure_future(coro_or_future)
。
import asyncio async def foo(char:str, count: int): for i in range(count): print(f"{char}-{i}") await asyncio.sleep(1) async def main(): task1 = asyncio.create_task(foo("A", 2)) task2 = asyncio.create_task(foo("B", 3)) task3 = asyncio.create_task(foo("C", 2)) await task1 await task2 await task3 if __name__ == '__main__': asyncio.run(main()) 复制代码
执行结果:
A-0 B-0 C-0 A-1 B-1 C-1 B-2 复制代码
总结
- 线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
- 对于CPU密集型的任务,则需要多个进程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。
- 在高并发下的最佳实践就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。