突破性能瓶颈:使用Asyncio构建高并发Python应用程序


是一种处理多个任务同时执行的编程方式,在Python中,asyncio是一种用于实现异步编程的强大工具。asyncio基于协程(coroutine)的概念,能够高效地处理I/O密集型任务。本文将介绍asyncio的基本原理和使用方法。

为啥需要asyncio

我们知道,在处理 I/O 操作时,使用多线程与普通的单线程相比,效率得到了极大的提高。既然这样,为什么还需要 Asyncio?

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

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

Sync VS Async

我们首先来区分一下 Sync(同步)和 Async(异步)的概念。

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

协程(Coroutines):Asyncio 使用协程来实现异步操作。协程是一种特殊的函数,使用 async 关键字定义。在协程中,可以使用 await 关键字暂停当前协程的执行,等待一个异步操作完成。

事件循环(Event Loop):事件循环是 Asyncio 的核心机制之一。它负责调度和执行协程,并处理协程之间的切换。事件循环会不断地轮询可执行的任务,一旦某个任务就绪(如 IO 完成或定时器到期),事件循环会将其放入执行队列并继续执行下一个任务。

异步任务(Async Tasks):在 Asyncio 中,我们通过创建异步任务来执行协程。异步任务由 asyncio.create_task() 函数创建,它将协程封装成一个可等待对象,并提交给事件循环进行处理。
异步 IO 操作:Asyncio 提供了一组异步的 IO 操作(如网络请求、文件读写等),这些操作可以通过协程和事件循环无缝地进行集成。通过使用异步 IO 操作,可以避免在等待 IO 完成时发生阻塞,提高程序的性能和并发性。

回调(Callbacks):Asyncio 也支持使用回调函数处理异步操作的结果。可以通过使用

asyncio.ensure_future() 函数将回调函数封装为一个可等待对象,并提交给事件循环进行处理。

并发执行:Asyncio 可以并发执行多个协程任务。事件循环会根据任务的就绪情况自动调度协程的执行,从而实现高效的并发编程。

总结起来,Asyncio 的工作原理是基于协程和事件循环的机制。通过使用协程进行异步操作,并由事件循环负责协程的调度和执行,Asyncio 实现了高效的异步编程模型。

协程与异步编程

协程是asyncio中的重要概念,它是一种轻量级的执行单位,可以在任务之间进行快速切换而无需线程切换的开销。协程可以通过async关键字定义,而await关键字用于暂停协程的执行,等待某个操作完成后再继续执行。

以下是一个简单的示例代码,演示了如何使用协程进行异步编程:

import asyncio
​
async def hello():
    print("Hello")
    await asyncio.sleep(1)  # 模拟耗时操作
    print("World")
​
# 创建一个事件循环
loop = asyncio.get_event_loop()
​
# 将协程加入事件循环并执行
loop.run_until_complete(hello())

在这个示例中,函数hello()是一个协程,通过async关键字进行定义。在协程内部,我们可以使用await来暂停协程的执行,这里使用asyncio.sleep(1)模拟一个耗时操作。通过run_until_complete()方法,将协程加入事件循环并运行。

异步I/O操作

asyncio主要用于处理I/O密集型任务,如网络请求、文件读写等操作。它提供了一系列的异步I/O操作API,可与await关键字配合使用,轻松实现异步编程。

以下是一个简单的示例代码,展示了如何使用asyncio进行异步网络请求:

import asyncio
import aiohttp
​
async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()
​
async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'https://www.example.com')
        print(html)
​
# 创建一个事件循环
loop = asyncio.get_event_loop()
​
# 将协程加入事件循环并执行
loop.run_until_complete(main())

在这个示例中,我们使用了aiohttp库进行网络请求。函数fetch()是一个协程,通过session.get()方法发起异步GET请求,并通过await关键字等待响应返回。函数main()是另一个协程,内部创建了一个ClientSession对象来重复使用,然后调用fetch()方法获取网页内容并打印。

注意:

看到这里我们使用了aiohttp而没有使用requests库,是因为requests 库并不兼容 Asyncio,但是 aiohttp 库兼容。

想用好 Asyncio,特别是发挥其强大的功能,很多情况下必须得有相应的 Python 库支持

并发执行多个任务

asyncio还提供了一些用于并发执行多个任务的机制,如asyncio.gather()asyncio.wait()等。下面是一个示例代码,展示了如何使用这些机制并发执行多个协程任务:

import asyncio
​
async def task1():
    print("Task 1 started")
    await asyncio.sleep(1)
    print("Task 1 finished")
​
async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")
​
async def main():
    await asyncio.gather(task1(), task2())
​
# 创建一个事件循环
loop = asyncio.get_event_loop()
​
# 将协程加入事件循环并执行
loop.run_until_complete(main())

在这个示例中,我们定义了两个协程任务task1()和task2(),它们都进行了一些耗时操作。协程main()通过asyncio.gather()同时启动这两个任务,并等待它们完成。通过并发执行,可以提高程序的执行效率。

如何选择?

实际项目中到底选择多线程还是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,则需要使用多进程来提高程序运行效率。

实战

输入一个列表,对于列表中的每个元素,我想计算 0 到这个元素的所有整数的平方和。

同步实现

import time
​
def cpu_bound(number):
    return sum(i * i for i in range(number))
​
​
def calculate_sums(numbers):
    for number in numbers:
        cpu_bound(number)
​
​
def main():
    start_time = time.perf_counter()
    numbers = [10000000 + x for x in range(20)]
    calculate_sums(numbers)
    end_time = time.perf_counter()
    print('Calculation takes {} seconds'.format(end_time - start_time))
​
​
if __name__ == '__main__':
    main()

执行时间需要Calculation takes 17.976343413000002 seconds

异步实现

concurrent.futures实现

import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
​
def cpu_bound(number):
    return sum(i * i for i in range(number))
​
​
def calculate_sums(numbers):
    with ProcessPoolExecutor() as executor:
        results = executor.map(cpu_bound, numbers)
        results = [result for result in results]
    print(results)
​
def main():
    start_time = time.perf_counter()
    numbers = [10000000 + x for x in range(20)]
    calculate_sums(numbers)
    end_time = time.perf_counter()
    print('Calculation takes {} seconds'.format(end_time - start_time))
​
​
if __name__ == '__main__':
    main()

执行时间需要Calculation takes 7.314132894999999 seconds.

在这个改进的代码中,我们使用 concurrent.futures.ProcessPoolExecutor 来创建一个进程池,然后通过 executor.map() 方法来提交任务和获取结果。

请注意,在使用 executor.map() 后,如果需要获取结果,可以将结果迭代为一个列表,或者使用其他方法对结果进行处理。

multiprocessing实现

import time
import multiprocessing
​
​
def cpu_bound(number):
    return sum(i * i for i in range(number))
​
​
def calculate_sums(numbers):
    with multiprocessing.Pool() as pool:
        pool.map(cpu_bound, numbers)
​
​
def main():
    start_time = time.perf_counter()
    numbers = [10000000 + x for x in range(20)]
    calculate_sums(numbers)
    end_time = time.perf_counter()
    print('Calculation takes {} seconds'.format(end_time - start_time))
​
​
if __name__ == '__main__':
    main()

执行用时Calculation takes 6.051121667 seconds

concurrent.futures.ProcessPoolExecutor 和 multiprocessing 都是 Python 中用于实现多进程并发的库,它们有一些区别。

  • 基于接口的封装:concurrent.futures.ProcessPoolExecutor 是 concurrent.futures 模块提供的一个高级接口,它对底层的多进程功能进行了封装,使得编写多进程代码更加简单。而 multiprocessing 则是 Python 的标准库之一,提供了完整的多进程支持,并允许直接操作进程。
  • API 使用方式:concurrent.futures.ProcessPoolExecutor 的使用方式类似于线程池,它通过提交可调用对象(如函数)到进程池中执行,并返回一个 Future 对象,可以用来获取执行结果。而 multiprocessing 提供了更底层的进程管理和通信接口,可以显式地创建、启动和控制进程,并使用多个进程之间的队列或管道进行通信。
  • 可扩展性和灵活性:由于 multiprocessing 提供了更底层的接口,因此它相对于 concurrent.futures.ProcessPoolExecutor 来说更加灵活。通过直接操作进程,可以对每个进程进行更细粒度的控制,如设置进程优先级、进程间共享数据等。而 concurrent.futures.ProcessPoolExecutor 更适合于简单的任务并行化,它隐藏了许多底层的细节,使得编写多进程代码更加简单和易用。
  • 跨平台支持:concurrent.futures.ProcessPoolExecutor 和 multiprocessing 都提供了跨平台的多进程支持,可以在各种操作系统上使用。

综上所述,concurrent.futures.ProcessPoolExecutor 是一个高级接口,封装了底层的多进程功能,适用于简单的多进程任务并行化。而 multiprocessing 是一个更底层的库,提供了更多的控制和灵活性,适用于需要精细控制进程的场景。

需要根据具体需求选择合适的库,如果只是简单的任务并行化,可以使用 concurrent.futures.ProcessPoolExecutor 来简化代码;如果需要更底层的控制和通信,可以使用 multiprocessing 库。

最后

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

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

尤其是在 I/O 操作 heavy 的场景下,Asyncio 比多线程的运行效率更高。因为 Asyncio 内部任务切换的损耗,远比线程切换的损耗要小;并且 Asyncio 可以开启的任务数量,也比多线程中的线程数量多得多。

但需要注意的是,很多情况下,使用 Asyncio 需要特定第三方库的支持,比如前面示例中的 aiohttp。而如果 I/O 操作很快,并不 heavy,那么运用多线程,也能很有效地解决问题。

asyncio是用于实现异步编程的Python库。
协程是asyncio的核心概念,通过async和await关键字实现异步操作。
asyncio提供了强大的异步I/O操作API,可轻松处理I/O密集型任务。
通过asyncio.gather()等机制,可以并发执行多个协程任务。

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/263696.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nature Commun.:物理所揭示原子分辨下的铁电涡旋畴的原位力学转变过程

通过复杂的晶格-电荷相互作用形成的铁电涡旋畴在纳米电子器件研发中具有巨大的应用潜力。实际应用中,如何在外界激励下操纵这类结构的拓扑状态是至关重要的。中国科学院物理研究所/北京凝聚态物理国家研究中心表面物理国家重点实验室与北京大学、湘潭大学和美国宾夕…

云原生文件存储 CFS 线性扩展到千亿级文件数,百度沧海·存储论文被 EuroSys 2023 录用

恭喜百度沧海云存储和中科大合作的论文《CFS: Scaling Metadata Service for Distributed File System via Pruned Scope of Critical Sections》(以下简称论文)被 EuroSys 2023 录用。 EuroSys 全称欧洲计算机系统会议(The European Confer…

ROS2 学习09--ros 中的通信接口的定义以及如何创建自定义msg、srv和action文件

在ROS系统中,无论话题还是服务,或者我们后续将要学习的动作,都会用到一个重要的概念——通信接口。 通信并不是一个人自言自语,而是两个甚至更多个人,你来我往的交流,交流的内容是什么呢?为了让…

【收藏】法律人办案必备检索网站最新汇总!附检索技巧

为什么要进行法律检索?无论你擅长的是做诉讼还是非诉讼业务,法律检索都是必备技能之一。只有做好法律检索才能制定出更加完备的策略报告,才能提供更加充实、可行、准确的方案。 一、数据库检索 1、alpha数据库 https://www.icourt.cc 已经用了3年的大数据库,听说最近降价了…

微前端样式隔离、sessionStorage、localStorage隔离

1、样式隔离 前端样式不隔离,会产生样式冲突的问题,这个点在qiankun也存在 子应用1修改一个样式 button {background: red!important; }其它应用也会受到影响 qiankun的css隔离方案(shadow dom) shadow …

c语言易错题之数据类型变换

1.题目 #include<stdio.h> int main() {int arr[]{1,2,3,4,5};short*p (short*)arr;int i 0;for(i0;i<4;i){*(pi)0;}for(i0;i<5;i){printf("%d ",arr[i];}return 0; }2.解析 这道题主要容易错在&#xff0c;大家会以为通过指针赋值的时候&#xff0c;…

pickle反序列化

文章目录 基础知识pickle简介可序列化对象object.__reduce__() 函数 pickle过程详细解读opcode简介pickletools 漏洞利用利用思路如何手写opcode 工具pker实战例题[MTCTF 2022]easypickle 基础知识 pickle简介 与PHP类似&#xff0c;python也有序列化功能以长期储存内存中的数…

docusaurus简介及使用心得

docusaurus简介 Docusaurus 是 Facebook 专门为开源项目开发者提供的一款易于维护的静态网站创建工具&#xff0c;使用 Markdown 即可更新网站。构建一个带有主页、文档、API、帮助以及博客页面的静态网站&#xff0c;只需5分钟。 同类竞品还有vuepress&#xff0c;docusaurus…

基于Linux下gcc学习C/C++——编译过程

前提&#xff1a;WSL2&#xff08;Ubuntu&#xff09;、gcc编译器。gcc安装命令&#xff1a; sudo apt-get install gcc 查看gcc版本&#xff1a; 目录 1、编译过程 1.1、预处理 1.2、编译与汇编 1.3、链接 2、gcc实验 2.1、预处理 2.2、编译 2.3、汇编 2.4、链接 1、…

Win系统修改Nginx配置结合内网穿透实现远程访问多个Web站点

文章目录 1. 下载windows版Nginx2. 配置Nginx3. 测试局域网访问4. cpolar内网穿透5. 测试公网访问6. 配置固定二级子域名7. 测试访问公网固定二级子域名 1. 下载windows版Nginx 进入官方网站(http://nginx.org/en/download.html)下载windows版的nginx 下载好后解压进入nginx目…

[笔记]netty随笔

记录使用过程中偶然发现的一些关键逻辑。先做记录&#xff0c;以后netty知识有一定体系再做整理 childGroup 服务器中有俩group&#xff0c;一个是parentGroup&#xff0c;负责处理链接请求&#xff0c;一个是childGroup&#xff0c;负责业务逻辑。 channelActive是在childG…

Spring中你一定要知道的@PostConstruct/@PreDestroy

文章目录 功能源码解析执行 功能 Spring中存在很多回调&#xff0c;但是执行他们的时机都不相同&#xff0c;也许大家用的最多的是InitializingBean.afterPropertiesSet&#xff0c;这个方法的作用如名称一样&#xff0c;是bean初始化后执行的一个回调操作&#xff0c;而PostC…

C语言学习NO.9-指针(一)内存和地址,指针变量和地址,指针变类型的意义,const修饰指针,指针运算,野指针,assret断言,指针的使用和传址调用

指针是什么&#xff1f; 指针是什么&#xff1f; 指针理解的2个要点&#xff1a; 1.指针是内存中一个最小单元的编号&#xff0c;也就是地址&#xff1b; 2.平时口语中说的指针&#xff0c;通常指的是指针变量&#xff0c;是用来存放内存地址的变量。 总结&#xff1a;指针就是…

MySQL中CASE when 实战

CASE 语法 CASEWHEN condition1 THEN result1WHEN condition2 THEN result2WHEN conditionN THEN resultNELSE result END; 将表中的内容转换为右边的形式&#xff1a; 1、创建表&#xff0c;创建数据 CREATE TABLEchapter10_7 (order_id VARCHAR(255) NULL,price VARCHAR(25…

手写题 - 实现一个带并发限制的异步调度器

题目 实现一个带并发限制的异步调度器 Scheduler&#xff0c;保证同时运行的任务最多有N个。 完善下面代码中的 Scheduler 类&#xff0c;使得以下程序能正确输出&#xff1a;class Scheduler {add(promiseCreator) { ... }// ... }const timeout (time) > new Promise(re…

从零学算法5

5.给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s “babad” 输出&#xff1a;“bab” 解释&#xff1a;“aba” 同样是符合题意的答案。 示例 2&…

Vue中为什么data属性是一个函数而不是一个对象?(看完就会了)

文章目录 一、实例和组件定义data的区别二、组件data定义函数与对象的区别三、原理分析四、结论 一、实例和组件定义data的区别 vue实例的时候定义data属性既可以是一个对象&#xff0c;也可以是一个函数 const app new Vue({el:"#app",// 对象格式data:{foo:&quo…

springboot学习笔记(五)

MybatisPlus进阶 1.MybatisPlus一对多查询 2.分页查询 1.MybatisPlus一对多查询 场景&#xff1a;我有一个表&#xff0c;里面填写的是用户的个人信息&#xff08;姓名&#xff0c;生日&#xff0c;密码&#xff0c;用户ID&#xff09;。我还有一个表填写的订单信息&#x…

Linux系统管理、服务器设置、安全、云数据中心

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 我们来快速了解liunx命令 文章目录 前言解析命令提示符linux的文件和目录文件和目录管理文件操作 进程管理命令系统管理网络管理 书籍推荐 本文以服务器最常用的CentOS为例 解析命令提示…

图片怎么转文字?这几个图片提取文字方法教会你!

在数字时代&#xff0c;我们每天都与大量的图片、文本信息打交道。当我们需要从图片中提取文字时&#xff0c;传统的方式可能是手动输入或者借助某些付费工具&#xff0c;今天介绍这三个工具不仅易于使用&#xff0c;而且效果卓越&#xff0c;我只需上传图片&#xff0c;工具便…