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

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
检索分析服务 Elasticsearch 版,2核4GB开发者规格 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机制来实现,如管道、共享内存或消息队列。

目录
相关文章
|
5天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
10天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
17 1
|
11天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
12天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
27 2
|
14天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
17天前
|
Java Unix 调度
python多线程!
本文介绍了线程的基本概念、多线程技术、线程的创建与管理、线程间的通信与同步机制,以及线程池和队列模块的使用。文章详细讲解了如何使用 `_thread` 和 `threading` 模块创建和管理线程,介绍了线程锁 `Lock` 的作用和使用方法,解决了多线程环境下的数据共享问题。此外,还介绍了 `Timer` 定时器和 `ThreadPoolExecutor` 线程池的使用,最后通过一个具体的案例展示了如何使用多线程爬取电影票房数据。文章还对比了进程和线程的优缺点,并讨论了计算密集型和IO密集型任务的适用场景。
37 4
|
17天前
|
调度 iOS开发 MacOS
python多进程一文够了!!!
本文介绍了高效编程中的多任务原理及其在Python中的实现。主要内容包括多任务的概念、单核和多核CPU的多任务实现、并发与并行的区别、多任务的实现方式(多进程、多线程、协程等)。详细讲解了进程的概念、使用方法、全局变量在多个子进程中的共享问题、启动大量子进程的方法、进程间通信(队列、字典、列表共享)、生产者消费者模型的实现,以及一个实际案例——抓取斗图网站的图片。通过这些内容,读者可以深入理解多任务编程的原理和实践技巧。
41 1
|
18天前
|
Linux 调度
探索操作系统核心:进程与线程管理
【10月更文挑战第24天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是计算机硬件与软件之间的桥梁,更是管理和调度资源的大管家。本文将深入探讨操作系统的两大基石——进程与线程,揭示它们如何协同工作以确保系统运行得井井有条。通过深入浅出的解释和直观的代码示例,我们将一起解锁操作系统的管理奥秘,理解其对计算任务高效执行的影响。
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
41 1
C++ 多线程之初识多线程