Python 核心技术与实战
景霄
Facebook 资深工程师
114324 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 47 讲
开篇词 (1讲)
Python 核心技术与实战
15
15
1.0x
00:00/00:00
登录|注册

22 | 并发编程之Asyncio

多进程版本
总结
选择规范
需要注意任务调度
兼容性问题
避免race condition
高效率
任务调度
Asyncio用法
Asyncio工作原理
Sync VS Async
思考题
多线程 vs Asyncio
Asyncio的缺陷
Asyncio的优势
异步编程
Asyncio
并发编程之Asyncio

该思维导图由 AI 生成,仅供参考

你好,我是景霄。
上节课,我们一起学习了 Python 并发编程的一种实现——多线程。今天这节课,我们继续学习 Python 并发编程的另一种实现方式——Asyncio。不同于协程那章,这节课我们更注重原理的理解。
通过上节课的学习,我们知道,在处理 I/O 操作时,使用多线程与普通的单线程相比,效率得到了极大的提高。你可能会想,既然这样,为什么还需要 Asyncio?
诚然,多线程有诸多优点且应用广泛,但也存在一定的局限性:
比如,多线程运行过程容易被打断,因此有可能出现 race condition 的情况;
再如,线程切换本身存在一定的损耗,线程数不能无限增加,因此,如果你的 I/O 操作非常 heavy,多线程很有可能满足不了高效率、高质量的需求。
正是为了解决这些问题,Asyncio 应运而生。

什么是 Asyncio

Sync VS Async

我们首先来区分一下 Sync(同步)和 Async(异步)的概念。
所谓 Sync,是指操作一个接一个地执行,下一个操作必须等上一个操作完成后才能执行。
而 Async 是指不同操作间可以相互交替执行,如果其中的某个操作被 block 了,程序并不会等待,而是会找出可执行的操作继续执行。
举个简单的例子,你的老板让你做一份这个季度的报表,并且邮件发给他。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Python并发编程有多种实现方式,其中之一是Asyncio。与多线程相比,Asyncio能够更有效地处理I/O操作,避免了线程切换的损耗和可能出现的race condition情况。Asyncio采用单线程和特殊的future对象来实现多个任务的并发执行,通过event loop控制任务的状态和执行顺序。在代码实现上,Asyncio使用async和await关键字来标识非阻塞操作,以及提供了一系列的函数来管理任务的执行。虽然Asyncio在处理I/O密集型任务时表现出色,但在实际应用中需要注意兼容性问题和任务调度的细节。总体而言,对于I/O密集型且I/O操作较慢的情况,使用Asyncio更为合适;而对于I/O操作较快的情况,多线程可能更适合。对于CPU密集型任务,则需要使用多进程来提高程序运行效率。 Asyncio是单线程的,但其内部event loop的机制,可以让它并发地运行多个不同的任务,并且比多线程享有更大的自主控制权。在I/O操作heavy的场景下,Asyncio比多线程的运行效率更高,因为Asyncio内部任务切换的损耗远比线程切换的损耗要小,并且Asyncio可以开启的任务数量也比多线程中的线程数量多得多。然而,使用Asyncio需要特定第三方库的支持,而在I/O操作很快的情况下,多线程也能有效地解决问题。 另外,文章还提到了并行编程(multi-processing),适用于CPU heavy的场景。为了比较程序的耗时,文章提出了一个需求:输入一个列表,对于列表中的每个元素,计算0到这个元素的所有整数的平方和。通过查阅资料,可以写出该需求的多进程版本,并进行程序耗时的比较。 总的来说,本文介绍了Asyncio的原理和用法,并比较了Asyncio和多线程各自的优缺点,同时提到了多进程适用于CPU heavy的场景。这些内容对于读者快速了解并发编程的不同实现方式以及其适用场景具有重要参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Python 核心技术与实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(72)

  • 最新
  • 精选
  • Jingxiao
    置顶
    思考题答案: import multiprocessing import time def cpu_bound(number): return sum(i * i for i in range(number)) def find_sums(numbers): with multiprocessing.Pool() as pool: pool.map(cpu_bound, numbers) if __name__ == "__main__": numbers = [10000000 + x for x in range(20)] start_time = time.time() find_sums(numbers) duration = time.time() - start_time print(f"Duration {duration} seconds")
    2019-07-02
    7
    29
  • 建强
    上网查询资料后,初步了解了多进程的一些知识,按照资料中的方法简单改写了一下程序,由于多进程方式时,不知什么原因,cpu_bound函数不能实时输出,所以就把cpu_bound改为返回字符串形式的结果,等所有的数计算完成后,再一并输出结果 ,程序中常规执行和多进程两种方式都有,并作了对比后发现,常规执行用时约23秒,多进程用时约6秒,两者相差4倍,程序如下,不足处请老师指正: #多进程演示 import multiprocessing import time def cpu_bound(number): return 'sum({}^2)={}'.format(number,sum(i * i for i in range(number))) def calculate_sums(numbers): results = [] print('-'*10+'串行执行开始:'+'-'*10) for number in numbers: results.append(cpu_bound(number)) print('-'*10+'串行执行结束,结果如下:'+'-'*10) for res in results: print(res) def multicalculate_sums(numbers): #创建有4个进程的进程池 pool = multiprocessing.Pool(processes=4) results = [] print('-'*10+'多进程执行开始:'+'-'*10) #为每一个需要计算的元素创建一个进程 for number in numbers: results.append(pool.apply_async(cpu_bound, (number,))) pool.close() #关闭进程池,不能往进程池添加进程 pool.join() #等待进程池中的所有进程执行完毕 print('-'*10+'多进程执行结束,结果如下:'+'-'*10) for res in results: print(res.get()) def main(): numbers = [10000000 + x for x in range(20)] #串行执行方式 start_time = time.perf_counter() calculate_sums(numbers) end_time = time.perf_counter() print('串行执行用时:Calculation takes {} seconds'.format(end_time - start_time)) #多进程执行方式 start_time = time.perf_counter() multicalculate_sums(numbers) end_time = time.perf_counter() print('多进程执行用时:Calculation takes {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main()

    作者回复: 很棒的例子,但是对计算密集型程序,你可以打开任务管理器的性能页,CPU 选择显示逻辑处理器,可以注意到串行执行和并行执行的不同。

    2019-10-21
    2
    6
  • szc
    能否举一些例子,哪些场景是IO密集型中的IOheavy, 那些是IO很快

    作者回复: 这个得看具体场景。比如大公司里相应业务爬虫的规模非常大,要抓取百万级的视频新闻信息流,这种就属于IO heavy。但是如果你只需要抓取10个网站的信息,并且网络连接良好,那么IO就很快

    2019-06-29
    1
  • 阿卡牛
    常听到阻塞,同步是不是就是阻塞地意思

    作者回复: 通俗来讲是

    2019-11-01
    2
  • helloworld
    总结多线程和协程之间的共同点和区别: 共同点: 都是并发操作,多线程同一时间点只能有一个线程在执行,协程同一时间点只能有一个任务在执行; 不同点: 多线程,是在I/O阻塞时通过切换线程来达到并发的效果,在什么情况下做线程切换是由操作系统来决定的,开发者不用操心,但会造成race condition; 协程,只有一个线程,在I/O阻塞时通过在线程内切换任务来达到并发的效果,在什么情况下做任务切换是开发者决定的,不会有race condition的情况; 多线程的线程切换比协程的任务切换开销更大; 对于开发者而言,多线程并发的代码比协程并发的更容易书写。 一般情况下协程并发的处理效率比多线程并发更高。
    2019-06-28
    3
    59
  • hlz-123
    1、单进程,老师的原程序,运行时间 Calculation takes 15.305913339 seconds 2、CPU并行方式,运行时间: Calculation takes 3.457259904 seconds def calculate_sums(numbers): with concurrent.futures.ProcessPoolExecutor() as executor: executor.map(cpu_bound,numbers) 3、多线程,cocurrent.futures,运行时间 Calculation takes 15.331446270999999 seconds def calculate_sums(numbers): with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: executor.map(cpu_bound,numbers) 4、异步方式,asyncio Calculation takes 16.019983702999998 seconds async def cpu_bound(number): print(sum(i * i for i in range(number))) async def calculate_sums(numbers): tasks=[asyncio.create_task(cpu_bound(number)) for number in numbers] await asyncio.gather(*tasks)
    2019-06-28
    4
    30
  • 天凉好个秋
    如果完成,则将其放到预备状态的列表; 如果未完成,则继续放在等待状态的列表。 这里是不是写的有问题? PS:想问一下,完成之后为什么还要放队列里?难道不应该从队列里移除吗?
    2019-06-28
    2
    8
  • transformation
    import time from concurrent import futures def cpu_bound(number): return sum(i * i for i in range(number)) def calculate_sums(numbers): for number in numbers: print(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)) def main_process(): start_time = time.perf_counter() numbers = [10000000 + x for x in range(20)] with futures.ProcessPoolExecutor() as pe: result = pe.map(cpu_bound, numbers) print(f"result: {list(result)}") end_time = time.perf_counter() print('multiprocessing Calculation takes {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main() main_process() ———————— 输出: 333333283333335000000 333333383333335000000 333333483333355000001 333333583333395000005 333333683333455000014 333333783333535000030 333333883333635000055 333333983333755000091 333334083333895000140 333334183334055000204 333334283334235000285 333334383334435000385 333334483334655000506 333334583334895000650 333334683335155000819 333334783335435001015 333334883335735001240 333334983336055001496 333335083336395001785 333335183336755002109 Calculation takes 15.771127400000001 seconds result: [333333283333335000000, 333333383333335000000, 333333483333355000001, 333333583333395000005, 333333683333455000014, 333333783333535000030, 333333883333635000055, 333333983333755000091, 333334083333895000140, 333334183334055000204, 333334283334235000285, 333334383334435000385, 333334483334655000506, 333334583334895000650, 333334683335155000819, 333334783335435001015, 333334883335735001240, 333334983336055001496, 333335083336395001785, 333335183336755002109] multiprocessing Calculation takes 4.7333084 seconds
    2019-06-28
    5
    8
  • Paul Shan
    sync是线性前后执行。 async是穿插执行,之所以要穿插,代码需要的资源不同,有的代码需要CPU,有的代码需要IO(例如网络),穿插以后,同时需要CPU和网络的代码可以同时执行,充分利用硬件。 具体到关键字 async 是表示函数是异步的,也就是来回穿插的起点(进入预备队列),await是表示调用需要IO,也就是进入等待队列的入口(函数开始调用)和出口(函数调用结束,重新进入预备队列)。
    2019-11-21
    3
  • 唐哥
    老师好,对于 Asyncio 来说,它的任务在运行时不会被外部的一些因素打断。不被打断是如何保证的?还有event loop是每次取出一个任务运行,当这个任务运行期间它就是只等待任务结束吗?不干其他事了吗?
    2019-07-01
    2
    3
收起评论
显示
设置
留言
72
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部