Python基于线程的并行和基于进程并行详解

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时数仓Hologres,5000CU*H 100GB 3个月
简介: 当涉及到并行编程时,Python标准库提供了两种不同的方式:基于线程的并行(threading)和基于进程的并行(multiprocessing)。下面我将从概念、性能、使用场景和底层实现等方面对它们进行解释和比较。

线程并行和进程并行的概念

基于线程的并行(线程并行)是指在一个进程中创建多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。由于多个线程共享同一个进程,因此线程之间的通信和同步相对容易实现。线程并行常用于处理I/O密集型任务,例如网络请求、文件读写等。

然而,线程并行也存在一些问题。首先,由于多个线程共享同一个进程,一个线程的错误可能会影响到其他线程的执行,导致整个进程崩溃或数据不一致。因此,在线程并行中需要特别关注线程安全性,使用适当的同步机制(如锁、信号量等)来保护共享资源的访问。其次,由于操作系统对进程和线程的管理方式不同,线程之间的切换和调度会带来一些开销,例如上下文切换的开销。此外,由于全局解释器锁(GIL)的存在,Python中的线程无法实现真正的并行执行,对于CPU密集型任务并没有性能上的优势。

importthreadingdefworker(num):
"""线程执行的任务"""print("Worker %d is running..."%num)
# 创建3个线程,每个线程执行worker函数threads= []
foriinrange(3):
t=threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
# 等待所有线程执行完毕fortinthreads:
t.join()
print("All threads are finished.")

上述代码使用threading模块创建了3个线程,并让每个线程执行worker函数。每个线程都会打印出一个相应的消息。

首先,worker函数定义了线程的具体任务,它接受一个参数num,并在函数体内打印相应的消息。

然后,在主程序中,一个空的线程列表threads被创建。接下来的循环中,通过threading.Thread类创建了3个线程对象,并将它们的目标函数指定为worker函数,同时传递一个参数i作为worker函数的参数。每个线程对象被添加到threads列表中,并通过调用start()方法启动线程。

接着,通过循环遍历threads列表,并调用join()方法等待所有线程执行完毕,这样主线程会阻塞在这里直到所有子线程都执行完成。

最后,打印出"All threads are finished."的消息,表示所有线程已经执行完毕。

基于进程的并行(进程并行)是指在操作系统中创建多个进程,每个进程都有自己独立的内存空间、文件描述符等资源。不同进程之间的通信和同步相对困难,需要使用特定的机制(如管道、共享内存等)进行进程间通信(IPC)。进程并行常用于处理CPU密集型任务,例如数据处理、图像处理等。

由于每个进程都是独立的,进程之间相互隔离,一个进程的错误不会影响到其他进程的执行。因此,在进程并行中,更容易实现安全和稳定的并行执行。然而,由于每个进程都有自己独立的内存空间,进程之间的数据共享和通信相对复杂,需要使用IPC机制来实现进程间的数据交换。

另外,由于操作系统对进程的管理方式不同于线程,进程之间的切换和调度会产生一些开销,例如创建和销毁进程的开销,以及进程间的数据传输开销。此外,每个进程都需要占用一定的内存资源,因此进程并行可能会在内存占用方面带来一些开销。

importmultiprocessingdefworker(num):
"""进程执行的任务"""print("Worker %d is running..."%num)
# 创建3个进程,每个进程执行worker函数processes= []
foriinrange(3):
p=multiprocessing.Process(target=worker, args=(i,))
processes.append(p)
p.start()
# 等待所有进程执行完毕forpinprocesses:
p.join()
print("All processes are finished.")

上述代码使用multiprocessing模块创建了3个进程,并让每个进程执行worker函数。每个进程都会打印出一个相应的消息。

首先,worker函数定义了进程的具体任务,它接受一个参数num,并在函数体内打印相应的消息。

然后,在主程序中,一个空的进程列表processes被创建。接下来的循环中,通过multiprocessing.Process类创建了3个进程对象,并将它们的目标函数指定为worker函数,同时传递一个参数i作为worker函数的参数。每个进程对象被添加到processes列表中,并通过调用start()方法启动进程。

接着,通过循环遍历processes列表,并调用join()方法等待所有进程执行完毕,这样主进程会阻塞在这里直到所有子进程都执行完成。

最后,打印出"All processes are finished."的消息,表示所有进程已经执行完毕。

通过代码不难看出,Thread和Process都是Python中用于实现并发编程的重要模块,它们之间有些许不同之处,但也有很多相同的地方。

线程并行和进程并行的性能

一般来说,threading模块的性能比multiprocessing模块要好一些。这是因为threading模块是在用户级别上创建线程的,而multiprocessing模块则是通过操作系统级别的进程来实现并发的。因此,在多核CPU的情况下,使用threading模块可以更好地利用多核CPU的性能,从而获得更好的并发性能。

另外,由于threading模块是在用户级别上创建线程的,因此它的开销相对较小,创建和销毁线程的速度也比较快。相比之下,multiprocessing模块需要操作系统内核创建和维护进程,因此它的开销相对较大,创建和销毁进程的速度也比较慢。

需要留意的是,因为CPython全局解释器锁的原因,同一时刻只有一个线程可以执行Python代码,官方文档中有特意标注:

如果你想让你的应用更好地利用多核心计算机的计算资源,推荐你使用 multiprocessing 或 concurrent.futures.ProcessPoolExecutor。 但是,如果你想要同时运行多个 I/O 密集型任务,则多线程仍然是一个合适的模型。

importthreadingimporttimeimportglobclassCSVReaderThread(threading.Thread):
def__init__(self, file_path, lock):
super().__init__()
self.file_path=file_pathself.result=Noneself.lock=lockdefrun(self):
# 获取锁self.lock.acquire()
try:
# 打开CSV文件并读取数据withopen(self.file_path, 'r') asf:
reader=csv.reader(f)
self.result= [rowforrowinreader]
# 打印读取的数据print(f"Data from {self.file_path} is completed.")
exceptExceptionase:
# 如果出现异常,则输出异常信息并释放锁,等待下一个线程继续执行print(f"Error reading {self.file_path}: {e}")
self.lock.release()
finally:
# 释放锁self.lock.release()
defread_csv_files():
files=glob.glob("*.csv") # 获取当前目录下所有CSV文件的路径# 创建多个线程并启动它们threads= []
max_threads=len(files) *5//4# 最多允许五个线程同时运行start_index=0end_index=max_threads+1whilestart_index<len(files):
ifend_index>len(files):
end_index=len(files)
else:
end_index+=1file_path=files[start_index:end_index] # 每次选取一个线程需要读取的文件路径范围t=CSVReaderThread(file_path[0], threading.Lock()) # 为每个线程创建一个锁对象threads.append(t)
t.start()
start_index+=end_index-start_index# 根据线程数量计算下一个线程开始的位置# 等待所有线程完成任务,包括中断的线程num_threads=len(threads)
foriinrange(num_threads):
threads[i].join() # 先等待正常完成的线程,再等待中断的线程

上述代码使用threading模块实现了一个读取多个CSV文件的并发任务。

首先定义了一个继承自threading.Thread的CSVReaderThread类,该类用于读取单个CSV文件的数据。在__init__方法中,传入文件路径和一个锁对象作为参数,并初始化了一些实例变量。

run方法是线程的主要执行逻辑。在执行之前,通过self.lock.acquire()获取锁,确保同一时间只有一个线程可以进入关键区域。然后尝试打开文件并读取其中的数据,使用csv.reader模块来解析CSV文件。读取的结果存储在self.result中。之后打印读取的数据,并在except块中处理可能发生的异常,打印错误信息并释放锁,以便其他线程可以继续执行。最后,无论是否发生异常,都通过self.lock.release()释放锁,确保下一个线程可以获取锁并执行。

read_csv_files函数是主程序的入口。首先使用glob.glob函数获取当前目录下所有的CSV文件路径,并存储在files列表中。接下来,创建一个空的线程列表threads,然后计算最大允许同时运行的线程数量,这里规定最多允许五个线程同时运行。使用start_index和end_index来迭代遍历files列表,并确定每个线程需要读取的文件范围。对于每个范围,创建一个CSVReaderThread对象,传入对应的文件路径和一个threading.Lock对象作为锁。将线程对象添加到threads列表中,并调用start()方法启动线程。

最后,使用num_threads记录线程的数量,通过循环遍历threads列表,依次调用join()方法等待所有线程完成任务,包括中断的线程。这样主线程会阻塞在这里,直到所有线程都执行完毕。

线程并行和进程并行的场景

Threading适用于小型、简单的并发任务,而Multiprocessing适用于大型、复杂的并发任务。

Threading

  • 线程之间的通信比较简单,通常只需要使用全局变量或共享数据结构来实现。
  • 线程之间的上下文切换比较频繁,因为每个线程都有自己的堆栈空间。
  • 线程的数量相对较少,通常在几十到几百个之间。

Multiprocessing

  • 需要在多个CPU核心上并行执行的任务,因为每个进程都有自己的独立内存空间和操作系统资源。
  • 需要在不同的计算机上运行的任务,因为每个进程都有自己的独立地址空间。
  • 需要高度同步的任务,因为每个进程都有自己的独立状态机。
  • 需要处理大量数据的任务,因为每个进程都可以使用独立的内存空间来存储数据。

比如我们可以使用threading模块创建多个线程来处理客户端请求,每个线程负责不同的部分,如接收请求、解析请求、处理响应;使用multiprocessing模块创建多个进程来处理大型数据集。每个进程都调用worker()函数来读取数据、进行处理和写入结果。我们可以使用multiprocessing模块提供的共享内存来传递数据和结果。

threading和multiprocessing的底层实现

Threading是通过使用线程来实现并发的。线程是轻量级的执行单位,它们在同一个进程中共享相同的内存空间。因为线程共享内存,所以在多线程编程中需要注意对共享资源的访问控制,以避免竞争条件和数据不一致问题。在Python中,线程通过threading模块来创建和管理。

Thread的底层实现是基于操作系统的原生线程机制,如POSIX线程(pthread)或Windows线程。这些原生线程由操作系统内核来管理调度。Python的解释器会在这些线程之间进行切换,以实现并发执行的效果。由于全局解释器锁(GIL)的存在,Python的多线程并不能实现真正的并行执行,GIL是一个Python语言级别的锁,它确保在任何时候只有一个线程可以执行Python字节码。

即使有多个CPU核心,也只有一个线程可以获得CPU时间片,因此无法充分利用多核CPU的优势。

相比之下,Multiprocessing是通过使用多个进程来实现并发的。每个进程都拥有自己独立的内存空间和解释器实例,它们通过进程间通信(IPC)来进行数据交换。由于每个进程都有自己的GIL,所以可以实现真正的并行执行。在Python中,进程通过multiprocessing模块来创建和管理。

Multiprocessing的底层实现依赖于操作系统提供的进程创建和管理机制。它使用操作系统级别的调度器来管理进程间的切换和调度。因为每个进程有独立的内存空间,所以它们之间的数据共享需要通过特定的IPC机制来实现,如管道、共享内存或消息队列。

目录
打赏
0
0
0
0
30
分享
相关文章
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
33 0
如何区分进程、线程和协程?看这篇就够了!
本课程主要探讨操作系统中的进程、线程和协程的区别。进程是资源分配的基本单位,具有独立性和隔离性;线程是CPU调度的基本单位,轻量且共享资源,适合并发执行;协程更轻量,由程序自身调度,适合I/O密集型任务。通过学习这些概念,可以更好地理解和应用它们,以实现最优的性能和资源利用。
90 11
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
53 6
【C语言】进程和线程详解
在现代操作系统中,进程和线程是实现并发执行的两种主要方式。理解它们的区别和各自的应用场景对于编写高效的并发程序至关重要。
113 6
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
Python中的多线程与多进程
本文将探讨Python中多线程和多进程的基本概念、使用场景以及实现方式。通过对比分析,我们将了解何时使用多线程或多进程更为合适,并提供一些实用的代码示例来帮助读者更好地理解这两种并发编程技术。
python中的线程和进程(一文带你了解)
欢迎来到瑞雨溪的博客,这里是一位热爱JavaScript和Vue的大一学生分享技术心得的地方。如果你从我的文章中有所收获,欢迎关注我,我将持续更新更多优质内容,你的支持是我前进的动力!🎉🎉🎉
59 0