Python并发编程Asyncio

目录

1. Asyncio是什么?

(1)Sync与Async

(2)Asyncio 工作原理

(3)Asyncio 用法

2. Asyncio是银弹吗?

3.多线程还是 Asyncio?

4.小结


我们来一起看下Python 并发编程的另一种实现方式——Asyncio,在处理 I/O 操作时,使用多线程与普通的单线程相比,效率得到了极大的提高。你可能会想,既然这样,为什么还需要 Asyncio?

诚然,多线程有诸多优点且应用广泛,但多线程也存在一定的局限性

  • 比如,多线程运行过程容易被打断,因此有可能出现 race condition 的情况;
  • 再如,线程切换本身存在一定的损耗,线程数不能无限增加,因此,如果你的 I/O 操作非常 heavy,多线程很有可能满足不了高效率、高质量的需求。

为了解决这些问题,Asyncio应运而生。

1. Asyncio是什么?

(1)Sync与Async

首先来区分一下 Sync(同步)和 Async(异步)的概念,如下:

  • Sync,是指操作一个接一个地执行,下一个操作必须等上一个操作完成后才能执行。
  • Async 是指不同操作间可以相互交替执行,如果其中的某个操作被 block 了,程序并不会等待,而是会找出可执行的操作继续执行。

(2)Asyncio 工作原理

事实上,Asyncio 和其他 Python 程序一样,是单线程的,它只有一个主线程,但是可以进行多个不同的任务(task),这里的任务,就是特殊的 future 对象。这些不同的任务,被一个叫做 event loop 的对象所控制。你可以把这里的任务,类比成多线程版本里的多个线程。

为了简化讲解这个问题,我们可以假设任务只有两个状态:一是预备状态;二是等待状态。所谓的预备状态,是指任务目前空闲,但随时待命准备运行。而等待状态,是指任务已经运行,但正在等待外部的操作完成,比如 I/O 操作。

在这种情况下,event loop 会维护两个任务列表,分别对应这两种状态;并且选取预备状态的一个任务(具体选取哪个任务,和其等待的时间长短、占用的资源等等相关),使其运行,一直到这个任务把控制权交还给 event loop 为止。

当任务把控制权交还给 event loop 时,event loop 会根据其是否完成,把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成。

  • 如果完成,则将其放到预备状态的列表;
  • 如果未完成,则继续放在等待状态的列表。

而原先在预备状态列表的任务位置仍旧不变,因为它们还未运行。

这样,当所有任务被重新放置在合适的列表后,新一轮的循环又开始了:event loop 继续从预备状态的列表中选取一个任务使其执行…如此周而复始,直到所有任务完成。

对于 Asyncio 来说,它的任务在运行时不会被外部的一些因素打断,因此 Asyncio 内的操作不会出现 race condition 的情况,这样你就不需要担心线程安全的问题了

(3)Asyncio 用法

以下载网站内容为例看段代码:(安装依赖库)

import asyncio
import aiohttp
import time


async def download_one(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            print('Read {} from {}'.format(resp.content_length, url))


async def download_all(sites):
    tasks = [asyncio.create_task(download_one(site)) for site in sites]
    await asyncio.gather(*tasks)


def main():
    sites = [
        'https://www.baidu.com/s?wd=Portal:Arts',
        'https://www.baidu.com/s?wd=Portal:History',
        'https://www.baidu.com/s?wd=Portal:Society',
        'https://www.baidu.com/s?wd=Portal:Biography',
        'https://www.baidu.com/s?wd=Portal:Mathematics',
        'https://www.baidu.com/s?wd=Portal:Technology',
        'https://www.baidu.com/s?wd=Portal:Geography',
        'https://www.baidu.com/s?wd=Portal:Science',
        'https://www.baidu.com/s?wd=Computer_science',
        'https://www.baidu.com/s?wd=Python_(programming_language)',
        'https://www.baidu.com/s?wd=Java_(programming_language)',
        'https://www.baidu.com/s?wd=PHP',
        'https://www.baidu.com/s?wd=Node.js',
        'https://www.baidu.com/s?wd=The_C_Programming_Language',
        'https://www.baidu.com/s?wd=Go_(programming_language)'
    ]
    start_time = time.perf_counter()
    asyncio.run(download_all(sites))
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))


if __name__ == '__main__':
    main()

这里的 Async 和 await 关键字是 Asyncio 的最新写法,表示这个语句 / 函数是 non-block 的,正好对应前面所讲的 event loop 的概念。如果任务执行的过程需要等待,则将其放入等待状态的列表中,然后继续执行预备状态列表里的任务。

Async 和 await 关键字是 Asyncio 的最新写法,表示这个语句 / 函数是 non-block 的,正好对应前面所讲的 event loop 的概念。如果任务执行的过程需要等待,则将其放入等待状态的列表中,然后继续执行预备状态列表里的任务。

主函数里的 asyncio.run(coro) 是 Asyncio 的 root call,表示拿到 event loop,运行输入的 coro,直到它结束,最后关闭这个 event loop。事实上,asyncio.run() 是 Python3.7+ 才引入的,相当于老版本的以下语句:

loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(coro)
finally:
    loop.close()

至于 Asyncio 版本的函数 download_all(),和之前多线程版本有很大的区别:


tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*task)

这里的asyncio.create_task(coro),表示对输入的协程 coro 创建一个任务,安排它的执行,并返回此任务对象。这个函数也是 Python 3.7+ 新增的,如果是之前的版本,你可以用asyncio.ensure_future(coro)等效替代。可以看到,这里我们对每一个网站的下载,都创建了一个对应的任务。

再往下看,asyncio.gather(*aws, loop=None, return_exception=False),则表示在 event loop 中运行aws序列的所有任务。当然,除了例子中用到的这几个函数,Asyncio 还提供了很多其他的用法,可以自行查阅了解下。

这段代码最后的输出结果用时只有 0.06s,效率比起之前的多线程版本,可以说是更上一层楼,充分体现其优势。

...省略...
Download
15
sites in 0.062144195078872144
seconds

2. Asyncio是银弹吗?

当然,Asyncio也不是银弹,它也有一定的局限性。实际工作中,想用好 Asyncio,特别是发挥其强大的功能,很多情况下必须得有相应的 Python 库支持。你可能注意到了,线程编程中,我们使可以使用 requests 库,但本文我们并没有使用,而是用了 aiohttp 库,原因就是 requests 库并不兼容 Asyncio,但是 aiohttp 库兼容。

Asyncio 软件库的兼容性问题,在 Python3 的早期一直是个大问题,但是随着技术的发展,这个问题正逐步得到解决。

另外,使用 Asyncio 时,因为你在任务的调度方面有了更大的自主权,写代码时就得更加注意,不然很容易出错。

举个例子,如果你需要 await 一系列的操作,就得使用 asyncio.gather();如果只是单个的 future,或许只用 asyncio.wait() 就可以了。那么,对于你的 future,你是想要让它 run_until_complete() 还是 run_forever() 呢?诸如此类,都是你在面对具体问题时需要考虑的。

3.多线程还是 Asyncio?

遇到实际问题时,多线程和 Asyncio 到底如何选择呢?总结大概如下:

  • 如果是 I/O bound,并且 I/O 操作很慢,需要很多任务 / 线程协同实现,那么使用 Asyncio 更合适。
  • 如果是 I/O bound,但是 I/O 操作很快,只需要有限数量的任务 / 线程,那么使用多线程就可以了。
  • 如果是 CPU bound,则需要使用多进程来提高程序运行效率。

4.小结

  • 不同于多线程,Asyncio 是单线程的,但其内部 event loop 的机制,可以让它并发地运行多个不同的任务,并且比多线程享有更大的自主控制权。
  • Asyncio 中的任务,在运行过程中不会被打断,因此不会出现 race condition 的情况。尤其是在 I/O 操作 heavy 的场景下,Asyncio 比多线程的运行效率更高。因为 Asyncio 内部任务切换的损耗,远比线程切换的损耗要小;并且 Asyncio 可以开启的任务数量,也比多线程中的线程数量多得多。
  • 但需要注意的是,很多情况下,使用 Asyncio 需要特定第三方库的支持,比如前面示例中的 aiohttp。而如果 I/O 操作很快,并不 heavy,那么运用多线程,也能很有效地解决问题。
上一篇:QSlider 常用API


下一篇:微服务从代码到k8s部署应有尽有系列(四、用户中心)