python核心技术与实战(十四):Asyncio实现并发

14. Asyncio实现并发

  • 14.1 Asyncio简介
  • 14.2 Asyncio的工作原理
  • 14.3 Asyncio使用示例
  • 14.4 Asyncio的缺陷
  • 14.4 选择多线程还是Asyncio
  • 总结

14.1 Asyncio简介

多线程已经可以带来较大的效率提升,那么我们还需要asyncio的原因是:

  • 多线程运行过程容易被打断,有可能出现race condition的情况
  • 线程切换本身存在一定的消耗,若I/O操作非常heavy,多线程很有可能满足不了高效率、高质量的需求

sync和async的概念区分

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

14.2 Asyncio的工作原理

Asyncio和python主程序一样,只有一个主线程,但是可以进行多个不同任务(特殊的future对象,可类比为多线程里的多个线程),这些任务被一个event loop的对象所控制

任务可分为两种状态:预备状态和等待状态

  • 预备状态:任务目前空闲,但随时待命准备运行

  • 等待状态:任务已经运行,但正在等待外部的操作完成,如I/O操作

event loop的执行过程:

  1. event loop会维护两个任务列表,分别对应预备和等待这两种状态
  2. 选取预备状态的一个任务(根据等待时间长短、占用资源等因素选取),使其运行直到该任务把控制权还给event loop为止
  3. 当接收到任务控制权后,event loop会根据其完成状态把任务放到对应的预备或等待状态的列表。遍历等待状态的列表,查看列表中的任务是否完成。已完成:放到预备状态的列表;未完成:继续放在等待状态的列表
  4. 原先在预备状态列表中的任务位置不变,因为它们仍未运行。当所有任务被重新放置在合适的列表后,新的一轮循环又开始了:event loop继续从预备状态的列表中选取一个任务使其执行…如此周而复始,直到所有任务都完成

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

14.3 Asyncio使用示例

以之前博文中下载网站内容的任务为例,其使用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://en.wikipedia.org/wiki/Portal:Arts',
        'https://en.wikipedia.org/wiki/Portal:History',
        'https://en.wikipedia.org/wiki/Portal:Society',
        'https://en.wikipedia.org/wiki/Portal:Biography',
        'https://en.wikipedia.org/wiki/Portal:Mathematics',
        'https://en.wikipedia.org/wiki/Portal:Technology',
        'https://en.wikipedia.org/wiki/Portal:Geography',
        'https://en.wikipedia.org/wiki/Portal:Science',
        'https://en.wikipedia.org/wiki/Computer_science',
        'https://en.wikipedia.org/wiki/Python_(programming_language)',
        'https://en.wikipedia.org/wiki/Java_(programming_language)',
        'https://en.wikipedia.org/wiki/PHP',
        'https://en.wikipedia.org/wiki/Node.js',
        'https://en.wikipedia.org/wiki/The_C_Programming_Language',
        'https://en.wikipedia.org/wiki/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()

## 输出
Read 63153 from https://en.wikipedia.org/wiki/Java_(programming_language)
Read 31461 from https://en.wikipedia.org/wiki/Portal:Society
Read 23965 from https://en.wikipedia.org/wiki/Portal:Biography
Read 36312 from https://en.wikipedia.org/wiki/Portal:History
Read 25203 from https://en.wikipedia.org/wiki/Portal:Arts
Read 15160 from https://en.wikipedia.org/wiki/The_C_Programming_Language
Read 28749 from https://en.wikipedia.org/wiki/Portal:Mathematics
Read 29587 from https://en.wikipedia.org/wiki/Portal:Technology
Read 79318 from https://en.wikipedia.org/wiki/PHP
Read 30298 from https://en.wikipedia.org/wiki/Portal:Geography
Read 73914 from https://en.wikipedia.org/wiki/Python_(programming_language)
Read 62218 from https://en.wikipedia.org/wiki/Go_(programming_language)
Read 22318 from https://en.wikipedia.org/wiki/Portal:Science
Read 36800 from https://en.wikipedia.org/wiki/Node.js
Read 67028 from https://en.wikipedia.org/wiki/Computer_science
Download 15 sites in 0.062144195078872144 seconds

上述代码中用到了Async和await关键字,表示所修饰的语句/函数是non-block的,这对应的便是上面提到的event loop的概念:若任务的执行过程中需要等待,则将其放入等待状态的列表中,然后继续执行预备状态列表中的任务。

主函数中的asyncio.run(coro)是Asyncio的root call,表示拿到event loop,运行输入的coro(即协程),直到它结束,最后关闭这个event loop。

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

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

这里的asyncio.create_task(coro),表示对输入的协程coro创建一个任务,安排它的执行,并返回此任务对象。可以看到,对每个网站,都创建了一个对应的任务。

再往下看,asyncio.gather(*aws, loop=None, return_exception=False),则表示在event loop 中运行*aws序列中的所有任务。

14.4 Asyncio的缺陷

在实际工作中,要想用好Asyncio,必须得有相应的python库支持。在之前的多线程例子中,我们用到的是requests库,而在这里使用的却是aiohttp库,原因就在于requests库与Asyncio不兼容,但aiohttp库兼容。但是兼容问题会随着版本的问题逐步减少。

此外,使用Asyncio使得我们在任务调度方面有更大的自主权,写代码时就得更加注意,否则容易出现错误。

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

14.4 选择多线程还是Asyncio

在面对具体问题时,我们可以按照以下伪代码的规范去选择用多线程还是asyncio:

if io_bound:
    if io_slow:
        print('Use Asyncio')
    else:
        print('Use multi-threading')
else if cpu_bound:
    print('Use multi-processing')
  • 如果是I/O bound,并且I/O操作很慢,需要很多任务/线程协同实现,那么选用Asyncio更合适(任务难度大)
  • 如果是I/O bound,但是I/O操作很快,且只需要有限任务/线程协同实现,那么选择多线程就行(任务难度小)
  • 如果是CPU bound,则需要选用多进程来提高程序运行效率

总结

不同于多线程,Asyncio是单线程的,但其内部event loop的机制,使得它可以并发运行多个不同的任务,并且比多线程享有更大的自主控制权。

Asyncio中的任务,在运行过程中不会被打断,因此不会出现race condition的情况。

在I/O heavy的情况下,Asyncio的运行效率比多线程更好。这是因为:

  • 任务和线程切换损耗:Asyncio内部任务切换的损耗,远比线程切换的损耗要小
  • 任务和线程数量:Asyncio可以开启的任务数,也远比多线程中的线程数量多得多

python核心技术与实战(十四):Asyncio实现并发_第1张图片

你可能感兴趣的:(python,python并发编程,Asyncio)