本篇介绍基于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. redis——redis的一些核心把握

    redis单线程,为什么比较快 单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程.redis能够快速执行的原因有三点: (1) 绝大 ...

  2. ACM-ICPC 2018 徐州赛区网络预赛 J. Maze Designer (最大生成树+LCA求节点距离)

    ACM-ICPC 2018 徐州赛区网络预赛 J. Maze Designer J. Maze Designer After the long vacation, the maze designer ...

  3. 2019 Petrozavodsk Winter Camp, Yandex Cup C. Diverse Singing 上下界网络流

    建图一共建四层 第一层为N个歌手 第二层为{pi,li} 第三层为{si,li} 第四层为M首歌 除了S和第一层与第三层与T之间的边为[1,INF] 其他边均为[0,1] #include<bi ...

  4. [易学易懂系列|rustlang语言|零基础|快速入门|(28)|实战5:实现BTC价格转换工具]

    [易学易懂系列|rustlang语言|零基础|快速入门|(28)|实战5:实现BTC价格转换工具] 项目实战 实战5:实现BTC价格转换工具 今天我们来开发一个简单的BTC实时价格转换工具. 我们首先 ...

  5. Oracle 11 安装教程(桌面类)

    准备文件: http://download.oracle.com/otn/nt/oracle11g/112010/win64_11gR2_database_1of2.zip http://downlo ...

  6. 嵌入式系统FreeRTOS — 互斥信号量

    互斥信号量可以在资源保护的时候很有帮助.用于控制在两个或多个任务间访问共享资源.任务1里面用互斥,那么任务2只能等任务1访问完再访问同一个变量. 比如全局变量double gADC_value[CH_ ...

  7. macOS关闭修改扩展名的提示

    关闭 defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false; killall Finder 开启 def ...

  8. parseInt parseFloat isNaN Number 区别和具体的转换规则及用法

    原文链接:https://blog.csdn.net/wulove52/article/details/84953998 在javascript 我经常用到,parseInt.parseFloat.N ...

  9. mysql: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

    https://www.cnblogs.com/jpfss/p/9734487.html (mysql.sock错误解决方案)

  10. PHP mysqli_fetch_lengths() 函数

    mysqli_fetch_lengths() 函数返回结果集中的字段长度. <?php // 假定数据库用户名:root,密码:123456,数据库:RUNOOB $con=mysqli_con ...