Python Asyncio 并发编程

使用多线程和普通的单线程相比,其运行效率会有极大的提高。但不得不说,多线程虽然有诸多优势,也存在一定的局限性:

多线程运行过程中容易被打断,还可能出现多个线程同时竞争同一资源的情况;
多线程切换本身存在一定的损耗,线程数不能无线增加,因此如果I\O操作非常频繁,多线程很有可能满足不了高效率、高质量的需求。

为了解决这些问题,Asyncio 并发编程应运而生。

在详细介绍 Asyncio 之前,要先搞清楚什么是同步,什么是异步。所谓同步,是指操作一个接一个地执行,下一个操作必须等上一个操作执行完成之后才能开始执行;而异步是指不同操作间可以相互交替执行,如果其中地某个操作被堵塞,程序并不会等待,而是会找出可执行的操作继续执行。

为了更好地区分同步和异步,这里举个例子,假设公司要我们做一份报表,并以邮件的方式提交,则分别以同步和异步的方式完成的过程如下:

如果按照同步的方式,应先向软件中输入各项数据,接下来等报表生成,再写邮件提交;
如果按照异步的方式,向软件中输出各项数据后,会先写邮件,等待报表生成后,暂停写邮件的工作去查看生成的报表,确认无误后在写邮件直到发送完毕。

了解了同步和异步(以及它们之间的区别)之后,接下来正式开始介绍 Asyncio。

什么是 Asyncio

事实上,Asyncio 和其他 Python 程序一样,是单线程的,它只有一个主线程,但可以进行多个不同的任务。这里的任务,指的就是特殊的 future 对象,我们可以把它类比成多线程版本里的多个线程。

这些不同的任务,被一个叫做事件循环(Event Loop)的对象所控制。所谓事件循环,是指主线程每次将执行序列中的任务清空后,就去事件队列中检查是否有等待执行的任务,如果有则每次取出一个推到执行序列中执行,这个过程是循环往复的。

为了简化讲解这个问题,可以假设任务只有两个状态:,分别是预备状态和等待状态:

预备状态是指任务目前空闲,但随时待命准备运行;
等待状态是指任务已经运行,但正在等待外部的操作完成,比如 I/O 操作。

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

当任务把控制权交还给事件循环对象时,它会根据其是否完成把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成:如果完成,则将其放到预备状态的列表;反之,则继续放在等待状态的列表。而原先在预备状态列表的任务位置仍旧不变,因为它们还未运行。

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

值得一提的是,对于 Asyncio 来说,它的任务在运行时不会被外部的一些因素打断,因此 Asyncio 内的操作不会出现竞争资源(多个线程同时使用同一资源)的情况,也就不需要担心线程安全的问题了。

如何使用Asyncio

讲完了 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.ensure_future(download_one(site)) for site in sites]
    await asyncio.gather(*tasks)

def main():
    sites = [
               'http://c.biancheng.net/view/9482.html',
               'http://c.biancheng.net/python/',
               'http://c.biancheng.net/linux_tutorial/'
    ]
    start_time = time.perf_counter()
   
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(download_all(sites))
    finally:
        loop.close()

    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
   
if __name__ == '__main__':
    main()


运行结果为:

Read None from http://c.biancheng.net/view/9482.html
Read None from http://c.biancheng.net/python/
Read None from http://c.biancheng.net/linux_tutorial/
Download 3 sites in 0.20080998099729186 seconds

注意,此程序运行前,需确保已安装好 aiohttp 模块,此模块可直接执行 pip install aiohttp 命令安装。
另外在主函数中,第 22-26 行代码表示拿到事件循环对象,并运行 download_all() 函数,直到其结束,最后关闭这个事件循环对象。

值得一提的,如果读者使用 Python 3.7 及以上版本,则 22-26 行代码可以直接用 asyncio.run(download_all(sites)) 来代替。

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

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

    注意,Python 3.7+ 版本之后,可以使用 asyncio.create_task(coro) 等效替代 asyncio.ensure_future(coro)。

asyncio.gather() 表示在事件循环对象中运行 aws 序列的所有任务。

可以看到,其输出结果显示用时只有 0.12s,比之前的多线程版本效率更高,充分体现其优势。
当然,除了例子中用到的这几个函数,Asyncio 还提供了很多其他的用法,你可以查看 Python 事件循环官方文档

Asyncio有缺陷吗?

通过以上的学习,明显看到了 Asyncio 的强大。但是,任何一种方案都不是完美的,都存在一定的局限性,Asyncio 同样如此。

实际工作中,想用好 Asyncio,特别是发挥其强大的功能,很多情况下必须得有相应的 Python 库支持。前面章节在学习多线程编程中使用的是 requests 库,但本节使用的是 aiohttp 库,原因在于 requests 库并不兼容 Asyncio,而 aiohttp 库兼容。Asyncio 软件库的兼容性问题,在 Python3 的早期一直是个大问题,但是随着技术的发展,这个问题正逐步得到解决。

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Python asyncio是一个用于编写并发代码的库。它提供了一种基于协程的并发编程模型,可以使编写异步代码更加简单和高效。通过使用async/await关键字,可以编写类似于同步代码的异步代码。 在使用asyncio进行并发编程时,可以使用事件循环(Event Loop)来调度协程任务的执行。事件循环是一个无限循环,不断地从任务队列中取出任务并执行。每当遇到一个需要等待的操作时,比如IO操作或者等待其他任务完成,事件循环会将当前任务挂起,并切换到下一个可执行的任务。一旦等待的操作完成,事件循环会恢复之前挂起的任务的执行。 下面是一个简单的示例代码,展示了如何使用asyncio进行并发编程: ```python import asyncio async def factorial(n): if n == 0: return 1 else: return n * await factorial(n - 1) async def main(): tasks = [factorial(i) for i in range(5)] results = await asyncio.gather(*tasks) print(results) if __name__ == '__main__': asyncio.run(main()) ``` 在上面的代码中,我们定义了一个factorial函数,使用递归计算阶乘。然后,我们定义了一个main函数,其中创建了一组协程任务,并使用asyncio.gather函数来等待所有任务完成并获取结果。最后,我们使用asyncio.run函数来运行主函数。 这只是一个简单的示例,实际应用中可能会涉及更复杂的并发场景。但是,通过使用asyncio,我们可以更方便地编写高效的并发代码,充分利用计算机的多核性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

self85

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值