Python 的 async


我第一次接触 异步编程 肯定是在JavaScript,第一次写延时函数的时候很奇怪为啥延时函数下面的代码会直接执行而不等待时间结束,然后就对异步编程有了一点点的了解,后面在看一个Python的qq机器人项目的时候发现里面使用了大量的异步函数,可惜没有去认真学,今天就来重新 补补 python 的异步实现~

什么是 异步 ? 什么是 同步?

举一个简单的例子,假设有一个爬虫程序,需要爬取一百张图片,同步的方法就是从第一张开始爬取,先发送请求,然后下载,保存,然后循环继续请求第二张,下载保存······ 异步的方式呢,就是遇到需要消耗大量时间的IO时会先去执行其他的函数,我们的下载保存就是整个爬虫程序中最需要时间的部分,可以通过异步编程,在发送第一个图片下载请求后等待下载图片的时间不闲着继续发送第二个请求,然后一直发送,发送请求的速度可以忽略,相当于100张图片在同时下载,不考虑自身网络情况,假设一张图片的下载需要1s,那同步的实现方式需要100s才能爬取全部图片,而异步只需要1s多一点,效率翻 N 倍!

怎样实现异步编程

从 Python 3.5 开始引入了 async 和 await 关键词:

import asyncio


async def a():
    print('a')
    await asyncio.sleep(2)
    print('a')


async def b():
    print('b')
    print('b')


asyncio.run(asyncio.wait([a(), b()]))

如上述代码块,在函数前声明async就表示当前函数为协程函数,函数内可以使用await 用于等待高 IO 代码的执行,其中的a函数在 输出第一行a之后会休眠2s,这时候不会去一直等他,而是会先去执行b函数,所以最终的输出结果为:

a
b
b
a

最后一行 asyncio.run(asyncio.wait([a(), b()])) 中的 asyncio.run() 这个API是Python 3.7 引入的,用来简便的去执行异步函数~

然后来详细的说说 python 的异步编程的原理

1.await

await 后面需要加可等待的对象,例如协程函数,Future,Task对象,IO 等待

import asyncio


async def main():
    print('1')
    await asyncio.sleep(2)
    print('2')


asyncio.run(main())

例如 上面的 await 后面跟的IO等待,这个时候如果有其他的协程函数在任务列表中,会先去执行其他函数,等待 await 后面的对象执行完成后 才会继续往下输出 2所以await简单来说就是一个等待标志,执行到这里的时候就可以先去执行别的函数了,等待执行完成后再继续往下执行,这也是异步的核心内容了。

2.Task 对象

可以在事件循环中并发的添加多个任务,也就是可以把多个协程函数当成任务放到一个任务列表中,在Python 3.7 以上版本中可以使用 asyncio.create_task API创建任务对象,如上面await中所说的,任务对象可以直接放在await关键词后面~

import asyncio


async def test_case():
    print(1)
    await asyncio.sleep(1)
    print(2)


async def main():
    task1 = asyncio.create_task(test_case())
    task2 = asyncio.create_task(test_case())
    await task1, task2


asyncio.run(main())

上面代码的输出结果为:

1
1
2
2

上面是使用了task1和task2来储存任务对象,我们可以使用函数列表来简化一下main函数的代码:

async def main():
    task_list = [asyncio.create_task(test_case()) for i in range(2)]
    await asyncio.wait(task_list)

直接把任务放在一个列表中,然后使用await去等待执行,但是列表对象显然不是之前说的await后面可放的内容,所以我们要使用asyncio.wait API来把任务列表转化成任务对象。

3.Future 对象

这是任务类的基类,任务对象就是在Future基础上实现的,Task内部的await处理时基于Future对象来的。

例如:

import asyncio


async def main():
    loop = asyncio.get_running_loop()
    future = loop.create_future()
    await future


asyncio.run(main())

首先通过 loop 创建一个当前事件循环,然后创一个空的future对象,await这个对象,这样会一直在等待获取futur的结果,显然我们没有让future处理返回结果,所以会一直等待下去。再看 下面的代码:

import asyncio


async def set_result(fut):
    await asyncio.sleep(1)
    fut.set_result('结果')


async def main():
    # 获取当前时间循环
    loop = asyncio.get_running_loop()
    # 创建 future 对象,没绑定任何行为就不会结束此任务
    future = loop.create_future()
    # 创建 task 对象,绑定了 set_result 函数
    # 此函数 1s 后会给future赋值结果,future就可以正常结束了
    await loop.create_task(set_result(future))
    res = await future
    print(res)


asyncio.run(main())

这时候,会在1s后正常输出个“结果”然后结束程序的运行。当然本段代码没有什么实际的意义,就是说明future和task的区别,我们很少会手动去用future,一般直接使用task即可。

4.concurrent.futures.Future对象

这个对象和上文的Future对象没有任何关系,如果你想使用线程池或者进程池来实现异步操作时用到的对象。比如可以通过它实现python异步模块和同步模块的混用。

例如通过使用线程池来实现异步:

import time
from concurrent.futures.thread import ThreadPoolExecutor


def f(val):
    time.sleep(1)
    print(val)


# 创建线程池
pool = ThreadPoolExecutor(max_workers=5)

for i in range(10):
    pool.submit(f, i)

输出的结果为:

021

4
3

75
8
9
6

可以看到数字完全乱了,这是因为多线程同时输出导致的,同样这样我们也实现了异步编程,而且用到的time库而不是asyncio.time,通过concurrent.futures.Future的线程池或者进程池我们可以把一些同步模块和一些异步模块混合使用。

5.实现 asyncio 和不支持异步的模块结合使用

比如一个爬虫案例:

async def download(url):
    loop = asyncio.get_event_loop()
    future = loop.run_in_executor(None, requests.get, url)
    res = await future
    return res.content

这样我们把requests.get放到线池中去,就可以实现异步爬虫啦


发表评论

您的电子邮箱地址不会被公开。