Python并发总结:多线程、多进程与异步编程
随着多核的发展,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类并传入目标函数及参数。
import threading
def worker():
print('线程正在执行')
# 创建线程
t = threading.Thread(target=worker)
# 启动线程
t.start()
# 等待线程执行完毕
t.join()
第二种方式通过继承threading.Thread类创建线程类,并重写run方法来定义线程执行的任务。
import threading
class MyThread(threading.Thread):
def run(self):
print(f'{self.name} 线程正在执行')
# 创建线程实例
my_thread = MyThread()
# 启动线程
my_thread.start()
# 等待线程执行完毕
my_thread.join()
最后一种方式是使用threading.ThreadPool实现线程池,在 Python 3 中,建议使用concurrent.futures模块中的ThreadPoolExecutor来实现线程池功能。
threading.ThreadPool已经标记过时,不建议在新的项目中再使用。
线程池的好处是可以管理一组线程,重用线程资源,减少线程创建和销毁的开销。
import concurrent.futures
def task(num):
print(f"执行任务 {num}")
return num * 2
# 创建线程池,最大线程数为3
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
# 提交任务
future_to_num = {executor.submit(task, num): num for num in range(5)}
for future in concurrent.futures.as_completed(future_to_num):
num = future_to_num[future]
try:
result = future.result()
except Exception as e:
print(f"任务 {num} 执行失败: {e}")
else:
print(f"任务 {num} 结果: {result}")
执行结果:
$ python.exe .\thread.py
执行任务 0
执行任务 1
执行任务 2
执行任务 3
任务 1 结果: 2
执行任务 4
任务 2 结果: 4
任务 0 结果: 0
任务 3 结果: 6
任务 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类并传入目标函数及参数来创建进程。
import multiprocessing
def worker():
print('进程正在执行')
if __name__ == '__main__':
# 创建进程
p = multiprocessing.Process(target=worker)
# 启动进程
p.start()
# 等待进程执行完毕
p.join()
第二种方式是通过继承multiprocessing.Process类,并重写run方法来定义进程执行的任务。
import multiprocessing
class MyProcess(multiprocessing.Process):
def run(self):
print(f'{self.name} 进程正在执行')
if __name__ == '__main__':
# 创建进程实例
my_process = MyProcess()
# 启动进程
my_process.start()
# 等待进程执行完毕
my_process.join()
最后一种方式是通过multiprocessing.Pool类创建一个进程池,自动分配任务给进程,提高资源利用率。
import multiprocessing
def task(num):
return num * 2
if __name__ == '__main__':
# 创建进程池,最大进程数为3
with multiprocessing.Pool(processes=3) as pool:
# 使用map方法并行执行任务
results = pool.map(task, range(5))
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关键字暂停协程执行,等待其他异步操作完成。
import asyncio
async def coroutine():
print('开始执行协程函数')
await asyncio.sleep(1)
print('协程函数执行结束')
if __name__ == '__main__':
asyncio.run(coroutine())
asyncio.run()用于运行最高层级的协程。
- 使用
asyncio.gather()函数可以同时运行多个协程。
import asyncio
async def coroutine1():
await asyncio.sleep(1)
print('协程1执行完毕')
async def coroutine2():
await asyncio.sleep(2)
print('协程2执行完毕')
if __name__ == "__main__":
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(asyncio.gather(coroutine1(), coroutine2()))
finally:
loop.close()
- 使用
async for对异步可迭代对象进行迭代。
import asyncio
async def async_generator():
for i in range(3):
await asyncio.sleep(1)
yield i
async def main():
async for num in async_generator():
print(num)
if __name__ == "__main__":
asyncio.run(main())
这种方式适用于处理异步产生的数据序列。
4. 总结
总的来看,
多线程是在一个进程里创建多个线程,共享资源,线程切换开销小,适合 I/O 密集型任务,像网络请求、文件读写。
它编程简单,能提高程序响应性,但因全局解释器锁,在 CPU 密集型任务中无法发挥多核优势,还存在线程安全问题。
多进程中每个进程有独立内存和资源,适合 CPU 密集型任务,能充分利用多核 CPU,稳定性高。
不过,进程创建和销毁开销大,进程间通信和数据共享复杂。
异步编程基于事件循环和协程,在单线程内实现异步。
它并发性能高,代码简洁,适合大量 I/O 密集型任务。但不适合 CPU 密集型任务,编程模型复杂,调试维护难。
简单来说,在开发时,I/O 密集型任务少用多线程,任务多用异步;CPU 密集型任务就选多进程;混合任务则按需组合。
Python并发总结:多线程、多进程与异步编程的更多相关文章
- 利用python yielding创建协程将异步编程同步化
转自:http://www.jackyshen.com/2015/05/21/async-operations-in-form-of-sync-programming-with-python-yiel ...
- C++程序员面试题目总结(涉及C++基础、多线程多进程、网络编程、数据结构与算法)
说明:C++程序员面试题目总结(涉及C++基础知识.多线程多进程.TCP/IP网络编程.Linux操作.数据结构与算法) 内容来自作者看过的帖子或者看过的文章,个人整理自互联网,如有侵权,请联系作者 ...
- 用 Python 3 的 async / await 做异步编程
前年我曾写过一篇<初探 Python 3 的异步 IO 编程>,当时只是初步接触了一下 yield from 语法和 asyncio 标准库.前些日子我在 V2EX 看到一篇<为什么 ...
- Python并发复习3 - 多进程模块 multiprocessing
python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程.Python提供了非常好用的多进程包multiprocessing,只需要定 ...
- Python Twisted系列教程2:异步编程初探与reactor模式
作者:dave@http://krondo.com/slow-poetry-and-the-apocalypse/ 译者:杨晓伟(采用意译) 这个系列是从这里开始的,欢迎你再次来到这里来.现在我们可 ...
- 流动python - 写port扫描仪和各种并发尝试(多线程/多进程/gevent/futures)
port扫描仪的原理非常easy.没有什么比操作更socket,能够connect它认为,port打开. import socket def scan(port): s = socket.socket ...
- 爬虫之多线程 多进程 自定义异步IO框架
什么是进程? 进程是程序运行的实例,是系统进行资源分配和调度的一个独立单位,它包括独立的地址空间,资源以及1个或多个线程. 什么是线程? 线程可以看成是轻量级的进程,是CPU调度和分派的基本单位. 进 ...
- python学习之多线程多进程
python基础 进程&线程 进程是一组资源的集合,运行一个系统就是打开了一个进程,如果同时打开了两个记事本就是开启了两个进程,进程是一个笼统的概念,进程中由线程干活工作,由进程统一管理 一个 ...
- C# 并发编程 (异步编程与多线程)
并发:同时做多件事情 多线程:并发的一种形式,它采用多个线程来执行程序. 并行处理:把正在执行的大量的任务分割成小块,分配给多个同时运行的线程.并行处理是多线程的一种,而多线程是并发的一种. 异步编程 ...
- 在Python中使用asyncio进行异步编程
对于来自JavaScript编码者来说,异步编程不是什么新东西,但对于Python开发者来说,async函数和future(类似JS的promise)可不是那么容易能理解的. Concurrency ...
随机推荐
- GoLand IDE 如何设置每次打开时先展示启动界面
GoLand IDE 如何设置每次打开时先展示启动界面 打开设置,在SystemSeting下进行如下操作即可
- 今日一学,5道大厂的Java基础面试题
前言 各种框架眼花缭乱,各种逻辑需求,CRUD.久而久之,写的1000行代码中都是if else,@autowired等等,等出去面试的时候,基础题不断,而且还是不常用,或者说不在意的,往往这些就容易 ...
- Jmeter并发线程场景下共享变量错乱问题解决
问题复现 问题描述 使用IF控制器获取前一个请求的后置脚本中设置的全局变量->并发线程下通过vars.get获取变量时,第一个线程和第二个线程获取的变量值一样->导致不同基础数据的请求入参 ...
- 2024-11-13:求出所有子序列的能量和。用go语言,给定一个整数数组nums和一个正整数k, 定义一个子序列的能量为子序列中任意两个元素之间的差值绝对值的最小值。 找出nums中长度为k的所有子
2024-11-13:求出所有子序列的能量和.用go语言,给定一个整数数组nums和一个正整数k, 定义一个子序列的能量为子序列中任意两个元素之间的差值绝对值的最小值. 找出nums中长度为k的所有子 ...
- 人工智能模型训练技术:随机失活,丢弃法,Dropout
前一篇:<探索训练人工智能模型的词汇大小与模型的维度> 序言:Dropout 是神经网络设计领域的一种技术,通常我们把它翻译成 随机失活 或者 丢弃法.如果训练神经网络的时候不用 Drop ...
- golang WEB框架Hertz --- 获取参数
安装Hertz命令行工具 请确保您的Go版本在1.15及以上版本,笔者用的版本是1.18 配置好GO的环境后,按照Hertz的命名行工具 go install github.com/cloudwego ...
- common-dbutils的使用
1. 介绍 commons-dbutils是Apache组织提供的一个开源 JDBC工具类库,能让我们更简单的使用JDBC.它是一个非常小的类包,花几分钟的时间就能掌握它的使用. 2. ...
- 前端项目部署之pushstate-server
pushstate-server 内部的原理是通过 connect 服务器,开启一个端口,将 dist/index.html 文件作为静态模板输出 这种方式可以将本地的项目打包成静态文件,以服务的方式 ...
- msde2000的关于无法访问lonle实例的master数据库恢复
某次关机重启后,lonele数据库实例无法访问,查看发现相应的服务(MSSQL$LONELE2.SQLAgent$LONELE2)无法启动. --------------------------- 服 ...
- uniapp 样式篇
1.全局变量 项目根目录的 uni.scss 文件是uni-app内置的常用样式变量,这个文件会自动引入,开发者可直接引用这个变了 文件默认已经定义了常用的变量,开发者也可以在此基础上继续添加 /* ...