一、协程的概念
协程,又称微线程,纤程。英文名Coroutine。是一种用户态的轻量级线程。
子程序,或者称为函数,在所有语言中都是层级调用的,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕,所以程序调用是通过栈实现的,一个线程就是执行一个子程序,子程序调用总是一个入口,一次返回,调用的顺序是明确的。而协程的调用和子程序不同。
线程是系统级别的,它由操作系统调度,而协程则是程序级别的,有程序根据需要自己调度,在一个线程中会有很多函数,我们把这些函数成为子程序,崽子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就成为协程。也就是说在同一线程内一段代码在执行过程中会终端然后跳转执行别的代码,接着在之前中断的地方继续开始执行。
二、使用yield实现生产者-消费者模型
1 def produce(c): 2 c.send(None) 3 for i in range(10): 4 print("生产者生产了%d号产品" % i) 5 c.send(i) 6 7 8 def consume(): 9 while True: 10 res = yield 11 print("消费者消费了%d号产品" % res) 12 13 14 if __name__ == '__main__': 15 c = consume() 16 produce(c)
注意:如果只是想来回切换,可以使用next()来执行生成器,如果还想往切换的生成器里面传递数据,可以使用send(num),但是要注意,第一次启动时,由于没有接收数据的对象,所以必须使用generator.send(None)来激活一下。
三、同步和异步
1、异步IO(asyncio)协程
使用异步IO,无非是提高我们写的软件系统的并发。这个软件系统,可以是网络爬虫,也可以是Web服务等等。
并发的方式有多种,多线程,多进程,异步IO。多线程和多进程更多应用于CPU密集型(计算密集型)的场景,比如科学计算的时间都耗费在CPU上,利用多核CPU来分担计算任务。多线程和多进程之间的场景切换和通讯代价很高,不适合IO密集型的场景。而异步IO就是非常适合IO密集型的场景,比如网络爬虫和Web服务。
IO就是读写磁盘、读写网络的操作,这种读写速度比读写内存、CPU缓存慢得多,前者的耗时是后者的成千上万倍甚至更多。这就导致IO密集型的场景99%的时间都花费在IO等待的时间上。异步IO就是把CPU从漫长的等待中释放出来的方法。
2、asyncio
asyncio是Python3.4版本引入的标准库,直接内置了对异步IO的支持,asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
(1)event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上,当满足事件发生的时候,就调用响应的协程函数。
(2)coroutine 协程:携程对象,指一个使用了async关键字定义的函数,它的调用不会立即执行,而是会返回一个协会对象。协程对象需要注册到事件循环中,由事件循环调用。
(3)task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协会进一步封装,其中包含任务的各种状态。
(4)future:代表将来执行或没有执行的任务的结果,它和task没有本质的区别 。
(5)async/await 关键字:python3.5用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。
四、创建task
1、使用async关键字定义一个协程对象
1 import asyncio 2 3 4 async def foo(): 5 pass 6 7 8 print(type(foo())) # <class 'coroutine'>
2、将协程对象(coroutine)封装为任务task
1 import asyncio 2 3 4 async def foo(): 5 pass 6 7 8 print(type(foo())) # <class 'coroutine'> 9 # 方式一 10 task1 = asyncio.ensure_future(foo()) 11 print(type(task1)) # <class '_asyncio.Task'> 12 # 方式二 13 loop = asyncio.get_event_loop() 14 task2 = loop.create_task(foo()) 15 print(type(task2)) # <class '_asyncio.Task'>
3、绑定回调
每个任务不管如何切换,总有执行完的时候,当执行完的时候一定有返回值(即使没有显示地写reeturn,也默认为None),可以给任务添加一个回调函数,当任务执行完毕后,就会执行函数,参数是future(执行完后的任务对象)
def done(future): # 使用future.result获取任务执行完成后的结果值 print("I am done, result: %s" %future.result()) task1 = asyncio.ensure_future(foo()) task1.add_done_callback(done)
4、简单的实现(任务、事件循环、注册运行)
1 import asyncio 2 3 4 async def foo(): 5 print("foo") 6 await asyncio.sleep(2) 7 return "foo" 8 9 10 print(type(foo())) # <class 'coroutine'> 11 # 方式一 12 task1 = asyncio.ensure_future(foo()) 13 print(type(task1)) # <class '_asyncio.Task'> 14 # 方式二 15 loop = asyncio.get_event_loop() 16 task2 = loop.create_task(foo()) 17 print(type(task2)) # <class '_asyncio.Task'> 18 19 tasks = [task1, task2] 20 # 当只运行一个任务时 21 # loop.run_until_complete(task1) 22 # 当运行多个任务时 23 loop.run_until_complete(asyncio.wait(tasks))
五、阻塞和await
使用async可以定义协程对象,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。协程遇到await,事件循环将会挂起该协程,执行别的协程,知道其他协程也挂起或者执行完毕,再进行下一个协程的执行。
耗时的操作一般是一些IO操作,例如网络请求,文件读取等。我们使用asyncio.sleep函数来模拟IO操作。协程的目的也是让这些IO操作异步化。
1、使用asyncio.sleep函数模拟IO操作
1 import asyncio 2 3 4 async def foo(): 5 print("foo") 6 await asyncio.sleep(2) 7 return "foo" 8 9 10 def done(future): 11 # 使用future.result获取任务执行完成后的结果值 12 print("I am done, result: %s" %future.result()) 13 task1 = asyncio.ensure_future(foo()) 14 task1.add_done_callback(done) 15 print(type(task1)) # <class '_asyncio.Task'> 16 # 方式二 17 loop = asyncio.get_event_loop() 18 task2 = loop.create_task(foo()) 19 print(type(task2)) # <class '_asyncio.Task'> 20 21 tasks = [task1, task2] 22 # 当只运行一个任务时 23 # loop.run_until_complete(task1) 24 # 当运行多个任务时 25 loop.run_until_complete(asyncio.wait(tasks))
为什么不是用time.sleep(2)来模拟阻塞操作呢?
注意注意注意:在async关键字定义的函数的实现语句中不可以出现不支持异步的模块代码,否则会销毁整个异步效果并报错。
2、支持网络异步操作的模块aiohttp
import asyncio, aiohttp async def get_response(url): async with aiohttp.ClientSession() as session: headers = { } async with await session.get(url=url, headers=headers) as response: result = await response.text() return result
支持异步网络请求的模块(aiohttp)
环境安装:pip install aiohttp
aiohttp的编码使用:
编写一个大致的架构:
1 async def get_response(url): 2 with aiohttp.ClientSession() as session: 3 headers = { 4 5 } 6 with session.get(url=url, headers=headers) as response: 7 result = await response.text() 8 return result
在架构中补充细节:
在每个with前加上async关键字
在每个阻塞操作前加上一个await关键字
完整代码:
1 import asyncio, aiohttp 2 3 4 async def get_response(url): 5 async with aiohttp.ClientSession() as session: 6 headers = { 7 8 } 9 async with await session.get(url=url, headers=headers) as response: 10 result = await response.text() 11 return result 12
六、异步和同步网络请求耗时比较
当发送10个请求时: 同步耗时 4.789260387420654秒 异步耗时:0.42975664138793945秒
当发送100个请求时:同步耗时 61.29694175720215秒 异步耗时:3.5729565620422363秒
1 import aiohttp, requests, time, asyncio 2 from threading import get_ident 3 headers = { 4 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36" 5 } 6 7 # 同步请求耗时 8 now = lambda : time.time() 9 start = now() 10 11 for i in range(100): 12 print(i) 13 requests.get(url="https://bj.lianjia.com/", headers=headers) 14 print("总耗时:%s秒" %(now() - start)) 15 # 4.789260387420654 10 16 # 61.29694175720215 100 17 18 # 异步请求耗时 19 now = lambda : time.time() 20 start = now() 21 22 async def get_response(url): 23 async with aiohttp.ClientSession() as session: 24 async with await session.get(url="https://bj.lianjia.com/", headers=headers) as response: 25 print(get_ident()) 26 27 28 loop = asyncio.get_event_loop() 29 30 tasks = [] 31 for i in range(100): 32 tasks.append(get_response("https://bj.lianjia.com/")) 33 34 loop.run_until_complete(asyncio.wait(tasks)) 35 36 37 print("总耗时:%s秒" %(now() - start)) 38 # 0.42975664138793945 10 39 # 3.5729565620422363 100
七、并发和并行
并发和并行是一个容易混淆的概念。并发通常指有多个任务需要同时进行,并行则是指同一时刻有多个任务执行。
asyncio实现并发
1 import asyncio, time, random 2 3 4 now = lambda : time.time() 5 6 start = now() 7 8 async def do_some_work(x): 9 print("I must wait %s seconds" % x) 10 await time.sleep(x) 11 return "%s" % x 12 13 14 tasks = [] 15 16 all_time = 0 17 18 loop = asyncio.get_event_loop() 19 20 for i in range(200): 21 x = random.randint(1, 7) 22 all_time += x 23 task = loop.create_task(do_some_work(x)) 24 tasks.append(task) 25 26 27 loop.run_until_complete(asyncio.wait(tasks)) 28 29 print(now() - start, all_time) 30 31 for t in tasks: 32 print(t.result())
八、协程嵌套(获取返回结果的三种方式)
使用async可以定义协程,协程用于耗时的IO操作,也可以封装更多的IO操作过程,这样就实现了嵌套的协程,即一个协程中await了另外一个协程,如此连接起来。
1 import asyncio, time, random 2 3 4 now = lambda : time.time() 5 6 start = now() 7 8 async def do_some_work(x): 9 print("I must wait %s seconds" % x) 10 await asyncio.sleep(x) 11 return "%s" % x 12 13 async def main(): 14 tasks = [] 15 16 for i in range(200): 17 x = random.randint(1, 7) 18 task = asyncio.create_task(do_some_work(x)) 19 tasks.append(task) 20 # 第一种方式 21 # dones, pendings = await asyncio.wait(tasks) 22 # for done in dones: 23 # print(done.result()) 24 # 第二种方式 25 # results = await asyncio.gather(*tasks) 26 # for result in results: 27 # # 处理结果 28 # pass 29 # 第三种方式:返回结果,并在任务都执行完毕后获取 30 # return await asyncio.gather(*tasks) 31 # 第四种方式 32 # return await asyncio.wait(tasks) 33 # 第五种 34 tasks = asyncio.as_completed(tasks) 35 for task in tasks: 36 result = await task 37 print(result) 38 39 loop = asyncio.get_event_loop() 40 41 # 第三种方式续 42 # results = loop.run_until_complete(main()) 43 # for result in results: 44 # print(result) 45 46 # 第四种方式续 47 # dones, pendings = loop.run_until_complete(main()) 48 # for done in dones: 49 # print(done.result())
九、协程停止
future对象有几个状态:
(1)Pending:future被创建时
(2)Running:事件循环调用时
(3)Done:调用完毕
(4)Cancelled:如果要停止事件循环,就需要先把task取消,可以使用asyncio.Task.all_task()获取事件循环的tasks,在逐一取消,关闭事件循环
1 import asyncio 2 from threading import get_ident 3 nums = [i for i in range(1000)] 4 5 6 async def cal_list(): 7 while True: 8 if len(nums) == 0: 9 return 10 a = nums.pop() 11 print("{}:{}".format(get_ident(),a)) 12 await asyncio.sleep(2) 13 14 15 loop = asyncio.get_event_loop() 16 17 tasks = [] 18 19 for i in range(3): 20 task = cal_list() 21 # next(task) 22 tasks.append(task) 23 24 try: 25 loop.run_until_complete(asyncio.wait(tasks)) 26 except KeyboardInterrupt as e: 27 print(asyncio.Task.all_tasks()) 28 for task in asyncio.Task.all_tasks(): 29 print(task.cancel()) 30 loop.stop() 31 loop.run_forever() 32 finally: 33 loop.close()