本篇介绍基于asyncio模块,实现单线程-多任务的异步协程

基本概念

协程函数

  • 协程函数: 定义形式为 async def 的函数;

aysnc

  • Python3.5+版本新增了aysncawait关键字,这两个语法糖让我们非常方便地定义和使用协程。

  • 如果一个函数的定义被async修饰后,则该函数就是一个特殊的函数(协程函数)

1
2
3
4
5
6
7
# 使用 async 关键字修饰函数后,调用该函数,但不会执行函数,而是返回一个coroutine协程对象
async def get_request(url):
print("正在请求: ", url)
sleep(1)
print('请求结束:', url) get_request('www.b.com')

运行分析:

  • 直接调用这个函数的话并不会被执行,也会出现一条警告 RuntimeWarning: coroutine 'get_request' was never awaited

  • 对于它的解释 官方文档 里提到,当协程程序被调用而不是被等待时(即执行 get_request('www.b.com') 而不是 await get_request('www.b.com') )或者协程没有通过 asyncio.create_task() 被排入计划日程(创建任务对象),asyncio 将会发出一条 RuntimeWarning

  • 当然 asyncio.create_task( get_request) 是py3.7中的,在之前的版本中是用到的 asyncio.ensure_future( get_request )

await

  • 在协程中如果要调用另一个协程就使用await要注意await关键字要在async定义的函数中使用,而反过来async函数可以不出现await
  • 如果一个对象可以在 await 语句中使用,那么它就是 可等待 对象。许多 asyncio API 都被设计为接受可等待对象。
  • 可等待 对象有三种主要类型: 协程, 任务Future.
    • 通过 ensure_futurecreate_task 函数打包协程对象即可得到任务。
    • Future 是一种特殊的 低层级 可等待对象,表示一个异步操作的 最终结果
      • 不用回调方法编写异步代码后,为了获取异步调用的结果,引入一个 Future 未来对象。Future 封装了与 loop 的交互行为,add_done_callback 方法向 epoll 注册回调函数,当 result 属性得到返回值后,会运行之前注册的回调函数,向上传递给 coroutine。
      • 通常情况下 没有必要 在应用层级的代码中创建 Future 对象
1
2
3
4
5
6
7
8
9
10
11
12
import asyncio

async def producer():
for i in range(1, 6):
print(f'生产:{i}')
await consumer(i) async def consumer(i):
print(f'消费:{i}') asyncio.run(producer())
# asyncio.run() 是py3.7更新出来的,在py3.7中,使用这个可以简单直接的运行 asyncio 程序。
  • asyncio.run() 函数用来运行最高层级的入口点 “main()” 函数,更多解释详见 官方文档

  • 此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。

协程对象

  • 协程对象*:调用 *协程函数 所返回的对象。

    • 特殊函数被调用后,函数内部的实现语句不会被立即执行,然后该函数调用会返回一个协程对象。
  • 结论:协程对象 == 特殊的函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async def get_request(url):
print("正在请求: ", url)
sleep(1)
print('请求结束:', url) # 函数调用:返回的就是一个协程对象
c = get_request('www.b.com')
print(c)
# <coroutine object get_request at 0x000002A6DFA026D0> # 创建3个协程对象
urls = [
'1.com', '2.com', '3.com'
]
coroutine_list = []
for url in urls:
c = get_request(url)
coroutine_list.append(c)
print(coroutine_list)
# [<coroutine object get_request at 0x0000022FE5313F10>, <coroutine object get_request at 0x0000022FE52426D0>, <coroutine object get_request at 0x0000022FE5313EB8>]

任务对象

  • 任务对象其实就是对协程对象的进一步封装
  • 任务 被用来设置日程以便 并发 执行协程。

结论:任务对象 == 高级的协程对象 == 特殊的函数调用

特性:可以绑定回调(爬虫中回调函数常用来做数据解析)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import asyncio
from time import sleep # 协程函数的定义
async def get_request(url):
print("正在请求: ", url)
sleep(1)
print('请求结束:', url) # 函数调用:返回的就是一个协程对象
c = get_request('www.b.com') # 创建一个任务对象:基于协程对象创建的
task = asyncio.ensure_future(c) # ensure_future 是py3.7之前的 # 创建3个任务对象
urls = [
'1.com', '2.com', '3.com'
]
task_list = [] # 存放多个任务对象的列表
for url in urls:
c = get_request(url)
task = asyncio.ensure_future(c)
task_list.append(task)

绑定回调

回调函数什么时候被执行?

  • 任务对象执行结束后执行

task.add_done_callback(func)

  • func必须要有一个参数,该参数表示的是该回调函数对应的任务对象
  • 回调函数的参数.result() : 任务对象对应的特殊函数执行结束的返回值。

事件循环对象

  • 作用:将其内部注册的任务对象进行异步执行。
  • 事件循环是异步编程的底层基石
  • 在py3.6中我们需要手动创建事件循环对象。
  • 在py3.7中,有了高层级的 asyncio 函数,例如 asyncio.run(),就很少有必要使用 低层级函数 来手动创建和关闭事件循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import asyncio
import time # 函数的定义
# 使用 async 关键字修饰函数后,调用该函数,但不会执行函数,而是返回一个coroutine协程对象
async def get_request(url):
print("正在请求: ", url)
# asyncio.sleep(1) # 阻塞1s没有成功
await asyncio.sleep(1) # 加上await关键字即可,这里的 await 表示等待
print('请求结束:', url) # 创建3个协程对象
urls = [
'1.com', '2.com', '3.com'
]
start = time.time() # 任务列表:存储多个任务对象 # py3.6
tasks = []
for url in urls:
c = get_request(url)
task = asyncio.ensure_future(c)
tasks.append(task)
# 获取当前事件循环,如果当前os线程没有设置并且 set_event_loop() 还没有被调用,asyncio创建一个新的事件循环
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks)) # 直接列表会报错,需要修饰以下,这里的 wait 表示挂起 print('总耗时:', time.time() - start) # py3.7
# 异步实现
# async def main():
# tasks = []
# for url in urls:
# c = get_request(url)
# task = asyncio.create_task (c)
# tasks.append(task)
# await asyncio.gather(*tasks)
# print('总耗时:', time.time() - start)
#
# asyncio.run(main()) # 当然这样的写法仍是同步
# async def main():
# for url in urls:
# c = get_request(url)
# task = asyncio.create_task(c)
# await task
# print('总耗时:', time.time() - start)
#
# asyncio.run(main())
  • 与py3.6相比,都是先做一个任务列表,然后py3.6需要手动创建事件循环对象get_event_loop 并使用 run_until_complete 来达到异步执行,而在py3.7中,gather会并发的执行传入的可等待对象并在run的调用下完成异步执行。所以在新版py3.7中,我们无需手动创建和关闭事件循环了。
  • py3.7用 create_task 代替 ensure_future。

编码流程

  • 定义协程函数

  • 创建协程对象

  • 封装任务对象

    • 绑定回调函数
  • 创建事件循环对象

  • 将任务对象注册到事件循环对象中,并且开启事件循环。

按照流程完整的py3.6代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import asyncio
import time # 定义协程函数
async def get_request(url):
print("正在请求: ", url)
# asyncio.sleep(1) # 阻塞1s没有成功
await asyncio.sleep(1) # 加上await关键字即可,这里的 await 表示等待
print('请求结束:', url)
return '我去回调啦' def parse(task): # task 表示与回调函数绑定的任务对象 / 给回调函数传入任务对象
print('i am task callback() !!!', task.result()) urls = [
'1.com', '2.com', '3.com'
]
start = time.time() # 任务列表:存储多个任务对象
tasks = []
for url in urls:
# 创建协程对象
c = get_request(url)
# 封装任务对象
task = asyncio.ensure_future(c)
# 绑定回调
task.add_done_callback(parse)
tasks.append(task)
# 创建事件循环对象
loop = asyncio.get_event_loop()
# 将任务对象注册到事件循环对象中,并且开启事件循环
loop.run_until_complete(asyncio.wait(tasks)) # 直接列表会报错,需要修饰以下,这里的 wait 表示挂起 print('总耗时:', time.time() - start)

note:在特殊函数内部的实现语句中不可以出现不支持异步的模块对应的代码,否则就会终止多任务异步协程的异步效果。

在py3.7中,则为

  • 定义协程函数

  • 定义 asyncio 程序的主入口

    • 创建协程对象
    • 封装任务对象
    • 绑定回调函数
  • asyncio.run(main())

按照流程完整的py3.7代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import asyncio
import time # 定义协程函数
async def get_request(url):
print("正在请求: ", url)
await asyncio.sleep(1)
print('请求结束:', url)
return '我去回调啦' def parse(task): # task 表示与回调函数绑定的任务对象 / 给回调函数传入任务对象
print('i am task callback() !!!', task.result()) urls = [
'1.com', '2.com', '3.com'
]
start = time.time() # 定义 asyncio 程序的入口点
async def main():
tasks = []
for url in urls:
# 创建协程对象
c = get_request(url)
# 封装任务对象
task = asyncio.create_task(c)
# 绑定回调函数
task.add_done_callback(parse)
tasks.append(task)
await asyncio.gather(*tasks)
print('总耗时:', time.time() - start) asyncio.run(main())

异步的本质

  • 按照注册顺序执行,遇到阻塞就会挂起,执行下一个任务。

  • 当上一个任务的阻塞结束后,就会继续执行该任务。

  • 真正的挂起是由 asyncio.wait(tasks) 做到的

图片来自: 谈谈Python协程技术的演进

图片来自 理解 Python asyncio

底层还没有理解,先把大佬的图粘过来慢慢研究

asyncio模块实现单线程-多任务的异步协程的更多相关文章

  1. python爬虫---单线程+多任务的异步协程,selenium爬虫模块的使用

    python爬虫---单线程+多任务的异步协程,selenium爬虫模块的使用 一丶单线程+多任务的异步协程 特殊函数 # 如果一个函数的定义被async修饰后,则该函数就是一个特殊的函数 async ...

  2. 爬虫必知必会(4)_异步协程-selenium_模拟登陆

    一.单线程+多任务异步协程(推荐) 协程:对象.可以把协程当做是一个特殊的函数.如果一个函数的定义被async关键字所修饰.该特殊的函数被调用后函数内部的程序语句不会被立即执行,而是会返回一个协程对象 ...

  3. 小爬爬4.协程基本用法&&多任务异步协程爬虫示例(大数据量)

    1.测试学习 (2)单线程: from time import sleep import time def request(url): print('正在请求:',url) sleep() print ...

  4. python爬虫--多任务异步协程, 快点,在快点......

    多任务异步协程asyncio 特殊函数: - 就是async关键字修饰的一个函数的定义 - 特殊之处: - 特殊函数被调用后会返回一个协程对象 - 特殊函数调用后内部的程序语句没有被立即执行 - 协程 ...

  5. 消息/事件, 同步/异步/协程, 并发/并行 协程与状态机 ——从python asyncio引发的集中学习

    我比较笨,只看用await asyncio.sleep(x)实现的例子,看再多,也还是不会. 已经在unity3d里用过coroutine了,也知道是“你执行一下,主动让出权限:我执行一下,主动让出权 ...

  6. 异步协程asyncio+aiohttp

    aiohttp中文文档 1. 前言 在执行一些 IO 密集型任务的时候,程序常常会因为等待 IO 而阻塞.比如在网络爬虫中,如果我们使用 requests 库来进行请求的话,如果网站响应速度过慢,程序 ...

  7. Python爬虫进阶 | 异步协程

    一.背景 之前爬虫使用的是requests+多线程/多进程,后来随着前几天的深入了解,才发现,对于爬虫来说,真正的瓶颈并不是CPU的处理速度,而是对于网页抓取时候的往返时间,因为如果采用request ...

  8. 深入理解协程(二):yield from实现异步协程

    原创不易,转载请联系作者 深入理解协程分为三部分进行讲解: 协程的引入 yield from实现异步协程 async/await实现异步协程 本篇为深入理解协程系列文章的第二篇. yield from ...

  9. python网络-多任务实现之协程(27)

    一.协程 协程,又称微线程,纤程.英文名Coroutine. 协程不是进程,也不是线程,它就是一个函数,一个特殊的函数——可以在某个地方挂起,并且可以重新在挂起处继续运行.所以说,协程与进程.线程相比 ...

随机推荐

  1. 洛谷 P2756 飞行员配对方案问题 (二分图/网络流,最佳匹配方案)

    P2756 飞行员配对方案问题 题目背景 第二次世界大战时期.. 题目描述 英国皇家空军从沦陷国征募了大量外籍飞行员.由皇家空军派出的每一架飞机都需要配备在航行技能和语言上能互相配合的2 名飞行员,其 ...

  2. springboot2.0入门(一)----springboot 简介

    一.springboot解决了什么? 避免了繁杂的xml配置,框架自动帮我们完成了相关的配置,当我们需要进行相关插件集成的时候,只需要将相关的starter通过相关的maven依赖引进,并可以进行相关 ...

  3. learning memchr func

    extern void *memchr(const void *buf, int ch, size_t count);   用法:#include <string.h> 功能:从buf所指 ...

  4. 小技巧——直接在目录中输入cmd然后就打开cmd命令窗口

    直接在目录中输入cmd然后就打开cmd命令窗口

  5. jQuery系列(八):jQuery的位置信息

    1.宽度和高度 (1):获取宽度 .width() 描述:为匹配的元素集合中获取第一个元素的当前计算宽度值.这个方法不接受任何参数..css(width) 和 .width()之间的区别是后者返回一个 ...

  6. Python回归分析五部曲(二)—多重线性回归

    基础铺垫 多重线性回归(Multiple Linear Regression) 研究一个因变量与多个自变量间线性关系的方法 在实际工作中,因变量的变化往往受几个重要因素的影响,此时就需要用2个或2个以 ...

  7. ie中兼容性问题

    由于项目要要兼容到ie8原本没有问题的代码一但用ie8打开js的报错找不到对象就都来了,其实总结起来就是ie越老的版本就越多方法名识别不到,那就少什么方法添加什么,比如说我的项目就要引入<scr ...

  8. Codeforces 869E. The Untended Antiquity (二维Fenwick,Hash)

    Codeforces 869E. The Untended Antiquity 题意: 在一张mxn的格子纸上,进行q次操作: 1,指定一个矩形将它用栅栏围起来. 2,撤掉一个已有的栅栏. 3,询问指 ...

  9. JavaWeb_(SSH)三大框架整合struts+hibernate+spring_Demo

    三大框架整合 一.SSH导包 二.书写Spring 三.书写Struts 四.整合Spring与Struts 五.书写(与整合)Hibernate.引入c3p0连接池并使用hibernate模板 六. ...

  10. 一、微服务(Microservices)【翻译】

    1.微服务 “微服务架构(Microservice Architecture)”一词在过去几年里广泛的传播,它用于描述一种设计应用程序的特别方式,作为一套独立可部署的服务.目前,这种架构方式还没有准确 ...