协程的概念
协程(coroutine)通常又称之为微线程或纤程,它是相互协作的一组子程序(函数)。所谓相互协作指的是在执行函数A时,可以随时中断去执行函数B,然后又中断继续执行函数A。注意,这一过程并不是函数调用(因为没有调用语句),整个过程看似像多线程,然而协程只有一个线程执行。协程通过yield关键字和 send()操作来转移执行权,协程之间不是调用者与被调用者的关系。
协程的优势在于以下两点:
1.执行效率极高,因为子程序(函数)切换不是线程切换,由程序自身
控制,没有切换线程的开销。
2.不需要多线程的锁机制,因为只有一个线程,也不存在竞争资源的问
题,当然也就不需要对资源加锁保护,因此执行效率高很多。
说明:协程适合处理的是I/O密集型任务,处理CPU密集型任务并不是它的长处,如果要提升CPU的利用率可以考虑“多进程+协程”的模式。
历史回顾:
1.Python 2.2:第一次提出了生成器(最初称之为迭代器)的概念
(PEP 255)。
2.Python 2.5:引入了将对象发送回暂停了的生成器,这一特性即生
3.Python 3.3:添加了yield from特性,允许从迭代器中返回任何
值(注意生成器本身也是迭代器),这样我们就可以串联生成器并且重
构出更好的生成器。
4.Python 3.4:引入asyncio.coroutine装饰器用来标记作为协程
的函数,协程函数和asyncio及其事件循环一起使用,来实现异步I/O
操作。
5.Python 3.5:引入了async和await,可以使用async def来定义
一个协程函数,这个函数中不能包含任何形式的yield语句,但是可以
使用return或await从协程中返回值。
协程运作的流程:
函数内部的中断和函数之间的更替执行,是靠send 和 yield 实现
的 send发送消息给yield,并且中端程序的执行, yield不仅可以接
收send发送的消息还可以中断程序,并且返回给send消息 ,"告诉该
你执行了!",send接收到消息后接着执行中断的程序,到结束进行下次
循环时循环到 send 下面流程就是重复的了
实例示范
-
生成器 - 数据的生产者
from time import sleep # 倒计数生成器 def countdown(n): while n > 0: yield n n -= 1 def main(): for num in countdown(5): print(f'Countdown: {num}') sleep(1) print('Countdown Over!') if __name__ == '__main__': main() 生成器还可以叠加来组成生成器管道,代码如下所示。 # Fibonacci数生成器 def fib(): a, b = 0, 1 while True: a, b = b, a + b yield a # 偶数生成器 def even(gen): for val in gen: if val % 2 == 0: yield val def main(): gen = even(fib()) for _ in range(10): print(next(gen)) if __name__ == '__main__': main()
-
协程 - 数据的消费者
示例01:from time import sleep # 生成器 - 数据生产者 def countdown_gen(n, consumer): consumer.send(None) while n > 0: consumer.send(n) n -= 1 consumer.send(None) # 协程 - 数据消费者 def countdown_con(): while True: n = yield if n: print(f'Countdown {n}') sleep(1) else: print('Countdown Over!') def main(): countdown_gen(5, countdown_con()) if __name__ == '__main__': main()
示例02:
# 消费者 快递员 from random import randint from time import sleep from myutils import coroutine # 激活消费者的装饰器 自己自动激活, 这样就不用在生成器中每次都激活了 @coroutine def create_delivery_man(name, capacity=1): buffer = [] # 不知道要消费多少东西 while True: # 接收派送给快递员的东西 size = 0 while size < capacity: pkg_name = yield if pkg_name: size += 1 buffer.append(pkg_name) print('%s正在接收%s' % (name, pkg_name)) else: break print('======%s正在派送%d件包裹' % (name, len(buffer))) sleep(3) buffer.clear() def create_package_center(consumer, max_packages): # 为了让消费者函数的代码停留在 n=yield 这一句,等着接收数据 # consumer.send(None) num = 0 while num <= max_packages: print('快递中心准备派送%d号包裹' % num) consumer.send('包裹-%d' % num) num += 1 if num % 10 == 0: sleep(5) # consumer.send(None) def main(): dm = create_delivery_man('杨二狗', 7) create_package_center(dm, 25) if __name__ == '__main__': main()
说明:上面代码中countdown_gen函数中的第1行consumer.send(None)是为了激活生成器,通俗的说就是让生成器执行到有yield关键字的地方挂起,当然也可以通过next(consumer)来达到同样的效果。如果不愿意每次都用这样的代码来“预激”生成器,可以写一个包装器来完成该操作,代码如下所示。
from functools import wraps def coroutine(fn): @wraps(fn) def wrapper(*args, **kwargs): gen = fn(*args, **kwargs) next(gen) return gen return wrapper
这样就可以使用@coroutine装饰器对协程进行预激操作,不需要再写重复代码来激活协程
-
异步I/O - 非阻塞式I/O操作
import asyncio @asyncio.coroutine def countdown(name, n): while n > 0: print(f'Countdown[{name}]: {n}') yield from asyncio.sleep(1) n -= 1 def main(): loop = asyncio.get_event_loop() tasks = [ countdown("A", 10), countdown("B", 5), ] loop.run_until_complete(asyncio.wait(tasks)) loop.close() if __name__ == '__main__': main()
-
async和await
import asyncio import aiohttp async def download(url): print('Fetch:', url) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: print(url, '--->', resp.status) print(url, '--->', resp.cookies) print('\n\n', await resp.text()) def main(): loop = asyncio.get_event_loop() urls = [ 'https://www.baidu.com', 'http://www.sohu.com/', 'http://www.sina.com.cn/', 'https://www.taobao.com/', 'https://www.jd.com/' ] tasks = [download(url) for url in urls] loop.run_until_complete(asyncio.wait(tasks)) loop.close() if __name__ == '__main__': main()
上面的代码使用了AIOHTTP这个非常著名的第三方库,它实现了HTTP客户端和HTTP服务器的功能,对异步操作提供了非常好的支持,有兴趣可以阅读它的官方文档