Python并发总结:多线程、多进程与异步编程

打印 上一主题 下一主题

主题 830|帖子 830|积分 2490

随着多核的发展,Python中并发编程也变得越来越广泛且发展很快。
一方面,Python提供了多种并发编程工具。
比如,传统的多线程,通过threading模块方便地创建和管理线程,可用于I/O麋集型任务;
多进程,使用multiprocessing模块充分使用多核CPU优势,适合CPU麋集型任务。
另一方面,随着异步编程的鼓起。asyncio库也让开发者能够编写高效的异步代码,提升程序性能,尤其在处置惩罚大量并发I/O操作场景体现出色。
不过,Python中全局表明器锁(GIL)为并发编程带来了不小的挑衅,目前社区正在积极探索绕过GIL的方法和优化策略,推动Python并发编程持续进步。
本篇计划一一先容怎样Python中使用多线程、多进程或异步的方式来编写程序。
1. 多线程

Python中多线程的模块是threading,早在Python 1.5 版本时就参加到标准库中了。
threading一直在发展,特别是进入Python3.x之后,
从Python3.3~Python3.13,险些每次Python的升级都伴随着threading的变化。
所以,使用时务必根据自己Python版本来正确使用threading的接口。
1.1. 使用场景和范围

Python的多线程广泛用于 I/O 麋集型的任务场景中,如网络请求、文件读写等,让程序在等待 I/O 操作时切换执行其他线程,从而提升整体服从。
随着应用场景拓展,多线程范围性也逐渐凸显。
最重要的是全局表明器锁(GIL),这是 Python 表明器的一个特性,同一时刻只有一个线程能执行 Python 字节码。
这导致在 CPU 麋集型任务中,多线程无法充分使用多核 CPU 优势,性能提升不明显乃至大概降低。
不过,尽管存在范围,多线程在 Python 生态中仍有重要职位。
开发者不断探索优化方法,如使用threading结合multiprocessing等其他并发模块,扬长避短。同时,新的 Python 版本也在尝试改进 GIL 机制,为多线程发展提供更多大概 。
1.2. 使用方式

在实际开发中,使用多线程重要有3种方式:
第一种方式是直接使用threading.Thread类创建线程,
这是最基本的方式,直接实例化threading.Thread类并传入目标函数及参数。
  1. import threading
  2. def worker():
  3.     print('线程正在执行')
  4. # 创建线程
  5. t = threading.Thread(target=worker)
  6. # 启动线程
  7. t.start()
  8. # 等待线程执行完毕
  9. t.join()
复制代码
第二种方式通过继续threading.Thread类创建线程类,并重写run方法来定义线程执行的任务。
  1. import threading
  2. class MyThread(threading.Thread):
  3.     def run(self):
  4.         print(f'{self.name} 线程正在执行')
  5. # 创建线程实例
  6. my_thread = MyThread()
  7. # 启动线程
  8. my_thread.start()
  9. # 等待线程执行完毕
  10. my_thread.join()
复制代码
最后一种方式是使用threading.ThreadPool实现线程池,在 Python 3 中,建议使用concurrent.futures模块中的ThreadPoolExecutor来实现线程池功能。
threading.ThreadPool已经标志过时,不建议在新的项目中再使用。
线程池的好处是可以管理一组线程,重用线程资源,淘汰线程创建和销毁的开销。
  1. import concurrent.futures
  2. def task(num):
  3.     print(f"执行任务 {num}")
  4.     return num * 2
  5. # 创建线程池,最大线程数为3
  6. with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
  7.     # 提交任务
  8.     future_to_num = {executor.submit(task, num): num for num in range(5)}
  9.     for future in concurrent.futures.as_completed(future_to_num):
  10.         num = future_to_num[future]
  11.         try:
  12.             result = future.result()
  13.         except Exception as e:
  14.             print(f"任务 {num} 执行失败: {e}")
  15.         else:
  16.             print(f"任务 {num} 结果: {result}")
复制代码
执行结果:
  1. $  python.exe .\thread.py
  2. 执行任务 0
  3. 执行任务 1
  4. 执行任务 2
  5. 执行任务 3
  6. 任务 1 结果: 2
  7. 执行任务 4
  8. 任务 2 结果: 4
  9. 任务 0 结果: 0
  10. 任务 3 结果: 6
  11. 任务 4 结果: 8
复制代码
2. 多进程

多线程模块multiprocessing自 Python 2.6 版本引入,随后在 Python 3.x 中持续发展。
在发展过程中,multiprocessing不断美满。它提供了简洁且强大的接口,让开发者能轻松创建和管理多个进程,充分使用多核 CPU 的优势,大幅提升 CPU 麋集型任务的处置惩罚服从。
它支持多种进程间通信方式,如队列、管道等,方便进程间的数据共享与同步。
2.1. 使用场景和范围

multiprocessing实用于CPU 麋集型计算,如科学计算、数据分析、图像处置惩罚等需要大量计算资源的任务。
当有多个独立任务需要同时执行时,也可以使用multiprocessing,例如批量文件处置惩罚、任务队列处置惩罚等。可以为每个任务分配一个进程,进步任务执行服从。
此外,在一些服务器应用中,也可以使用多进程让主进程处置惩罚请求的同时,其他进程负责背景任务,如数据缓存更新、日记记录等,从而避免阻塞主线程,提升应用的响应速度 。
不过,multiprocessing也存在一些范围性。
由于每个进程都有独立的内存空间,进程间数据共享和通信相对复杂,需要额外的机制和同步操作,大概带来性能损耗。
并且,创建和销毁进程的开销较大,频繁地创建和销毁进程会影响程序的整体性能。
此外,它的使用场景相对受限,不实用于简单的并发任务,相比多线程,在 I/O 麋集型任务中优势不明显,因为多线程在 I/O 等待时能切换执行其他任务,多进程则会耗费更多资源。
2.2. 使用方式

这里也先容使用multiprocessing的3种常用的方式:
第一种是直接使用 Process 类,通过实例化multiprocessing.Process类并传入目标函数及参数来创建进程。
  1. import multiprocessing
  2. def worker():
  3.     print('进程正在执行')
  4. if __name__ == '__main__':
  5.     # 创建进程
  6.     p = multiprocessing.Process(target=worker)
  7.     # 启动进程
  8.     p.start()
  9.     # 等待进程执行完毕
  10.     p.join()
复制代码
第二种方式是通过继续multiprocessing.Process类,并重写run方法来定义进程执行的任务。
  1. import multiprocessing
  2. class MyProcess(multiprocessing.Process):
  3.     def run(self):
  4.         print(f'{self.name} 进程正在执行')
  5. if __name__ == '__main__':
  6.     # 创建进程实例
  7.     my_process = MyProcess()
  8.     # 启动进程
  9.     my_process.start()
  10.     # 等待进程执行完毕
  11.     my_process.join()
复制代码
最后一种方式是通过multiprocessing.Pool类创建一个进程池,自动分配任务给进程,进步资源使用率。
  1. import multiprocessing
  2. def task(num):
  3.     return num * 2
  4. if __name__ == '__main__':
  5.     # 创建进程池,最大进程数为3
  6.     with multiprocessing.Pool(processes=3) as pool:
  7.         # 使用map方法并行执行任务
  8.         results = pool.map(task, range(5))
  9.         print(results)
复制代码
这三种使用方式看起来和上一节中的threading都差不多,不过,它们底层的处置惩罚是完全不一样的,
multiprocessing会为每个任务单独创建一个进程去执行;而threading中的全部任务都是在同一个进程中执行的。
3. 异步

异步模块asyncio的汗青比上面的两个模块要迟很多,它在Python 3.4 版本中被首次引入。
在Python 3.5时, 引入了async和await关键字,让异步代码的编写更加简洁、易读,大大提升了异步编程的体验,推动了asyncio的广泛应用。
3.1. 使用场景和范围

asyncio实用于下面几种对并发处置惩罚要求高的场景:

  • 网络爬虫:在爬取多个网页时,asyncio能在等待响应的同时,继续发送其他请求,大大进步爬取服从,缩短获取大量数据的时间。
  • 网络服务端开发:处置惩罚高并发的客户端连接,如构建谈天服务器、实时数据推送服务等。它能异步处置惩罚每个客户端请求,避免阻塞,确保服务器高效运行。
  • I/O麋集型任务:如文件读写、数据库操作等。asyncio可在等待I/O操作完成时执行其他任务,淘汰整体等待时间,提升程序性能。
当然,asyncio的优势明显,但也存在一些范围性。
一方面,由于它基于单线程,在处置惩罚 CPU 麋集型任务时性能欠佳,无法充分使用多核 CPU 的优势。
另一方面,异步编程模型相对复杂,代码调试和维护难度较高,需要开发者对异步概念有深入明白,否则容易出现逻辑错误。
此外,asyncio与一些传统的同步库大概存在兼容性问题,在集成现有代码时大概会碰到困难。
3.2. 使用方式

asyncio是比较新的模块,它的使用方式重要有:

  • 定义一个协程函数,使用async def关键字声明,在函数内部使用await关键字暂停协程执行,等待其他异步操作完成。
  1. import asyncio
  2. async def coroutine():
  3.     print('开始执行协程函数')
  4.     await asyncio.sleep(1)
  5.     print('协程函数执行结束')
  6. if __name__ == '__main__':
  7.     asyncio.run(coroutine())
复制代码
asyncio.run()用于运行最高层级的协程。

  • 使用asyncio.gather()函数可以同时运行多个协程。
  1. import asyncio
  2. async def coroutine1():
  3.     await asyncio.sleep(1)
  4.     print('协程1执行完毕')
  5. async def coroutine2():
  6.     await asyncio.sleep(2)
  7.     print('协程2执行完毕')
  8. if __name__ == "__main__":
  9.     try:
  10.         loop = asyncio.get_running_loop()
  11.     except RuntimeError:
  12.         loop = asyncio.new_event_loop()
  13.         asyncio.set_event_loop(loop)
  14.     try:
  15.         loop.run_until_complete(asyncio.gather(coroutine1(), coroutine2()))
  16.     finally:
  17.         loop.close()
复制代码

  • 使用async for对异步可迭代对象进行迭代。
  1. import asyncio
  2. async def async_generator():
  3.     for i in range(3):
  4.         await asyncio.sleep(1)
  5.         yield i
  6. async def main():
  7.     async for num in async_generator():
  8.         print(num)
  9. if __name__ == "__main__":
  10.     asyncio.run(main())
复制代码
这种方式实用于处置惩罚异步产生的数据序列。
4. 总结

总的来看,
多线程是在一个进程里创建多个线程,共享资源,线程切换开销小,适合 I/O 麋集型任务,像网络请求、文件读写。
它编程简单,能进步程序响应性,但因全局表明器锁,在 CPU 麋集型任务中无法发挥多核优势,还存在线程安全问题。
多进程中每个进程有独立内存和资源,适合 CPU 麋集型任务,能充分使用多核 CPU,稳定性高。
不过,进程创建和销毁开销大,进程间通信和数据共享复杂。
异步编程基于事件循环和协程,在单线程内实现异步。
它并发性能高,代码简洁,适合大量 I/O 麋集型任务。但不适合 CPU 麋集型任务,编程模型复杂,调试维护难。
简单来说,在开发时,I/O 麋集型任务少用多线程,任务多用异步;CPU 麋集型任务就选多进程;混合任务则按需组合。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

雁过留声

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表