asyncio 是 Python 中的异步IO库,用来编写并发协程,适用于IO阻塞且需要大量并发的场景,例如爬虫、文件读写。

asyncio 在 Python3.4 被引入,经过几个版本的迭代,特性、语法糖均有了不同程度的改进,这也使得不同版本的 Python 在 asyncio 的用法上各不相同,显得有些杂乱,以前使用的时候也是本着能用就行的原则,在写法上走了一些弯路,现在对 Python3.7+ 和 Python3.6 中 asyncio 的用法做一个梳理,以便以后能更好的使用。

协程与asyncio

协程,又称微线程,它不被操作系统内核所管理,而完全是有程序控制,协程切换花销小,因而有更高的性能。

协程可以比作子程序,不同的是,执行过程中协程可以挂起当前状态,转而执行其他协程,在适当的时候返回来接着执行,协程间的切换不需要涉及任何系统调用或任何阻塞调用,完全由协程调度器进行调度。

Python 中以 asyncio 为依赖,使用 async/await 语法糖进行协程的创建和使用,如下 async 语法创建一个协程函数:

async def work():
pass

在协程中除了普通函数的功能外最主要的作用就是:使用 await 语法等待另一个协程结束,这将挂起当前协程,直到另一个协程产生结果再继续执行:

async def work():
await asyncio.sleep(1)
print('continue')

asyncio.sleep() 是 asyncio 包内置的协程函数,这里模拟耗时的IO操作,上面这个协程执行到这一句会挂起当前协程而去执行其他协程,直到sleep结束,当有多个协程任务是,这种切换会让它们的IO操作并行处理。

注意,执行一个协程函数并不会真正的运行它,而是会返回一个协程对象,要使协程真正的运行,需要将它们加入到事件循环中运行,官方建议 asyncio 程序应当有一个主入口协程,用来管理所有其他的协程任务:

async def main():
await work()

在 Python3.7+ 中,运行这个 asyncio 程序只需要一句:asyncio.run(main()) ,而在 Python3.6 中,需要手动获取事件循环并加入协程任务:

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

事件循环就是一个循环队列,对其中的协程进行调度执行,当把一个协程加入循环,这个协程创建的其他协程都会自动加入到当前事件循环中。

其实协程对象也不是直接运行,而是被封装成一个个待执行的 Task ,大多数情况下 asyncio 会帮我们进行封装,我们也可以提前自行封装 Task 来获得对协程更多的控制权,注意,封装 Task 需要当前线程有正在运行的事件循环,否则将引 RuntimeError,这也就是官方建议使用主入口协程的原因,如果在主入口协程之外创建任务就需要先手动获取事件循环然后使用底层的方法 loop.create_task(),任务创建后便有了状态,可以查看运行情况,查看结果,取消任务等:

async def main():
task = asyncio.create_task(work())
print(task)
await task
print(task) #----执行结果----#
<Task pending name='Task-2' coro=<work() running at d:\tmp\code\asy.py:5>>
<Task finished name='Task-2' coro=<work() done, defined at d:\tmp\code\asy.py:5> result=None>

asyncio.create_task() 是 Python3.7 加入的高层级API,在 Python3.6,需要使用低层级API asyncio.ensure_future() 来创建 Future,Future 也是一个管理协程运行状态的对象,与 Task 没有本质上的区别。

并发协程

通常,一个含有一系列并发协程的程序写法如下(Python3.7+):

import asyncio
import time async def work(num: int):
'''
一个工作协程,接收一个数字,将它 +1 后返回
'''
print(f'working {num} ...')
await asyncio.sleep(1) # 模拟耗时的IO操作
print(f'{num} -> {num+1} done')
return num + 1 async def main():
'''
主协程,创建一系列并发协程并运行它们
'''
# 任务队列
tasks = [work(num) for num in range(0, 5)]
# 并发执行队列中的协程并等待结果返回
results = await asyncio.gather(*tasks)
print(results) if __name__ == "__main__":
asyncio.run(main())

并发运行多个协程任务的关键就是 asyncio.gather(*tasks),它接受多个协程任务并将它们加入到事件循环,所有任务都运行完成后会返回结果列表,这里我们也没有手动封装 Task,因为 gather 函数会自动封装。

并发运行还有另一个方法 asyncio.wait(tasks),它们的区别是:

  • gather 比 wait 更加高层,gather 可以将任务分组,一般优先使用 gather:
tasks1 = [work(num) for num in range(0, 5)]
tasks2 = [work(num) for num in range(5, 10)]
group1 = asyncio.gather(*tasks1)
group2 = asyncio.gather(*tasks2)
results1, results2 = await asyncio.gather(group1, group2)
print(results1, results2)
  • 在某些定制化任务需求的时候,可以使用 wait:
# Python3.8 版本后,直接向 wait() 传入协程对象已弃用,必须手动创建 Task
tasks = [asyncio.create_task(work(num)) for num in range(0, 5)]
done, pending = await asyncio.wait(tasks)
for task in tasks:
if task in done:
print(task.result())
for p in pending:
p.cancel()

Tips

  • await 语句后必须是一个 可等待对象 ,可等待对象主要有三种:Python协程,Task,Future。通常情况下没有必要在应用层级的代码中创建 Future 对象。
  • 在 asyncio 程序中使用同步代码虽然并不会报错,但是也失去了并发的意义,例如网络请求,如果使用仅支持同步的 requests,在发起一次请求后在收到响应结果之前不能发起其他请求,这样要并发访问多个网页时,即使使用了 asyncio,在发送一次请求后切换到其他协程还是会因为同步问题而阻塞,并不能有速度上的提升,这时候就需要其他支持异步请求库如 aiohttp
  • 关于 asyncio 的更多更详细的操作见 官方文档

Python协程之asyncio的更多相关文章

  1. python协程之动态添加任务

    https://blog.csdn.net/qq_29349715/article/details/79730786 python协程只能运行在事件循环中,但是一旦事件循环运行,又会阻塞当前任务.所以 ...

  2. python协程--asyncio模块(基础并发测试)

    在高并发的场景下,python提供了一个多线程的模块threading,但似乎这个模块并不近人如意,原因在于cpython本身的全局解析锁(GIL)问题,在一段时间片内实际上的执行是单线程的.同时还存 ...

  3. Python协程之Gevent模块

    背景 进程是操作系统分配资源的最小单位,每个进程独享4G的内存地址空间,因此进程内数据是安全的,检查间的通信需要使用特定的方法.同理,正是因为进程是数据安全的,所以导致进程的切换是一个很麻烦效率不高的 ...

  4. 练习PYTHON协程之GREENLET

    STACKLESS就算了,了解一下原理即可. GREENLET,GEVENT,EVENTLET这些,比较好测试,还是都 撸一次,得个印象. 测试代码都是网上的大路货. from greenlet im ...

  5. python并发编程之asyncio协程(三)

    协程实现了在单线程下的并发,每个协程共享线程的几乎所有的资源,除了协程自己私有的上下文栈:协程的切换属于程序级别的切换,对于操作系统来说是无感知的,因此切换速度更快.开销更小.效率更高,在有多IO操作 ...

  6. python协程(yield、asyncio标准库、gevent第三方)、异步的实现

    引言 同步:不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的. 例如购物系统中更新商品库存,需要用"行锁"作为通信信号,让不同的更新 ...

  7. python协程详解,gevent asyncio

    python协程详解,gevent asyncio 新建模板小书匠 #协程的概念 #模块操作协程 # gevent 扩展模块 # asyncio 内置模块 # 基础的语法 1.生成器实现切换 [1] ...

  8. python异步编程之asyncio

    python异步编程之asyncio   前言:python由于GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病.然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上千倍的效率, ...

  9. 理解Python协程:从yield/send到yield from再到async/await

    Python中的协程大概经历了如下三个阶段:1. 最初的生成器变形yield/send2. 引入@asyncio.coroutine和yield from3. 在最近的Python3.5版本中引入as ...

随机推荐

  1. PHP uniqid() 函数

    实例 生成一个唯一的 ID: <?phpecho uniqid();?>高佣联盟 www.cgewang.com 定义和用法 uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ...

  2. [草稿]Skill 中的map

    https://www.cnblogs.com/yeungchie/ Skill 中的map map mapc mapcan mapcar mapcon mapinto maplist

  3. ThinkPHP6 核心分析之应用程序初始化

    runWithRequest () 方法 在 Http 类的 run() 方法中,得到 think\Request 类的实例后,程序接着执行 $response = $this->runWith ...

  4. jmeter分布式踩得坑汇总

    一.普通的配置文件基本都能网上搜索资料,这里就简单记录: a.jmeter.properties几处修改:1.remote_hosts=master压力机Ip;2.server_port,开启服务器端 ...

  5. 当asp.net core偶遇docker一(模型验证和Rabbitmq 三)

    继续上一篇 上一篇,从core方式实现了一个Rabbitmq发送队列消息的接口,我们现在需要在模型验证里面加入验证失败就发送消息的部分 [AttributeUsage(AttributeTargets ...

  6. demo1 动态显示view或弹框 动态隐藏view或弹框

    实现界面如上所示: 有一个弹框,弹框上边有一个关闭按钮,点击按钮,可以关闭弹框.点击弹框的周围区域也可以关闭按钮. 点击上边的隐藏弹框也可以关闭按钮. 在实现功能的基础上,以动画的形式展示跟隐藏. 思 ...

  7. 节点操作 - DOM编程

    1. 获取节点 1.1 直接获取节点 父子关系: element.parentNode element.firstChild/element.lastChild element.firstElemen ...

  8. 【模式识别与机器学习】——PART2 机器学习——统计学习基础——Regularized Linear Regression

    来源:https://www.cnblogs.com/jianxinzhou/p/4083921.html 1. The Problem of Overfitting (1) 还是来看预测房价的这个例 ...

  9. Java Redis系列3(Jedis的使用+jedis连接池技术)

    Jedis的使用 什么是Jedis? 一款Java操作redis数据库的工具 使用步骤 1.下载redis所需的java包 2.使用步骤 import org.junit.Test; public c ...

  10. C#开发笔记之01-为什么开源框架会大量的使用protected virtual?

    C#开发笔记概述 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/957 访问. 我们在很多开源框架中会经常看到prote ...