一、多进程(Multiprocessing)
多进程是指在操作系统中同时运行多个进程,每个进程都有自己独立的内存空间和系统资源。Python 的multiprocessing
模块提供了多进程编程的支持。
优点
- 可以利用多核 CPU 的优势,并行执行任务,提高程序的运行效率。
- 各个进程之间相互独立,一个进程的崩溃不会影响其他进程。
缺点
- 进程的创建和销毁开销较大。
- 进程之间的通信和数据共享相对复杂。
代码示例
import multiprocessing
# 定义一个简单的任务函数
def worker(num):
"""进程要执行的任务"""
# 打印当前工作进程的编号,表示该进程已开始执行任务
print(f'Worker {num} started')
# 计算当前工作进程编号的平方,作为任务的结果
result = num * num
# 打印当前工作进程的编号和任务结果,表示该进程已完成任务
print(f'Worker {num} finished with result: {result}')
# 返回任务结果
return result
if __name__ == '__main__':
# 创建一个进程池,最多同时运行3个进程
pool = multiprocessing.Pool(processes=3)
# 向进程池提交任务
results = [pool.apply_async(worker, args=(i,)) for i in range(5)]
# 关闭进程池,不再接受新的任务
pool.close()
# 等待所有进程完成任务
pool.join()
# 获取每个进程的返回结果
output = [p.get() for p in results]
# 打印最终结果,即所有进程执行任务后的返回结果
print(f'Final results: {output}')
二、充分利用CPU
multiprocessing.Pool
并不是最多只能同时运行 3 个进程,在创建 Pool
对象时,processes
参数指定了进程池中的进程数量,示例代码中设置为 3 只是一个示例,你可以根据实际需求调整该参数。
如何全面使用 16 核 CPU?
当你的 CPU 是 16 核时,你可以将 processes
参数设置为 16,这样进程池就会创建 16 个进程,理论上可以充分利用 CPU 的多核性能。不过,在实际应用中,还需要考虑系统资源的其他使用情况(如内存),有时候并不一定需要将进程数量设置得和 CPU 核心数完全一致。
下面是一个示例代码,展示了如何将 processes
参数设置为 16 来充分利用 16 核 CPU:
import multiprocessing
# 定义一个简单的任务函数,模拟 CPU 密集型任务
def worker(num):
"""进程要执行的任务"""
# 打印当前进程的开始信息
print(f'Worker {num} started')
# 模拟一些 CPU 密集型计算
# 初始化结果变量
result = 0
# 循环从0到999999
for i in range(1000000):
# 将当前循环变量i累加到结果变量result中
result += i
# 打印当前进程的完成信息
print(f'Worker {num} finished')
# 返回累加结果
return result
if __name__ == '__main__':
# 获取 CPU 的核心数
cpu_count = multiprocessing.cpu_count()
print(f"CPU 核心数: {cpu_count}")
# 创建一个进程池,进程数量设置为 CPU 核心数
pool = multiprocessing.Pool(processes=cpu_count)
# 向进程池提交任务,这里假设要执行 20 个任务
results = [pool.apply_async(worker, args=(i,)) for i in range(20)]
# 关闭进程池,不再接受新的任务
pool.close()
# 等待所有进程完成任务
pool.join()
# 获取每个进程的返回结果
output = [p.get() for p in results]
# 打印最终结果列表的长度
print(f'Final results length: {len(output)}')
代码解释
multiprocessing.cpu_count()
:用于获取当前系统的 CPU 核心数。multiprocessing.Pool(processes=cpu_count)
:创建一个进程池,进程数量设置为 CPU 核心数,这样可以充分利用多核 CPU 的性能。pool.apply_async(worker, args=(i,))
:向进程池异步提交任务,worker
是任务函数,args
是传递给任务函数的参数。pool.close()
:关闭进程池,不再接受新的任务。pool.join()
:等待所有进程完成任务。p.get()
:获取每个进程的返回结果。
注意事项
- 虽然设置进程数量为 CPU 核心数可以充分利用多核性能,但在实际应用中,还需要考虑内存等其他系统资源的使用情况。如果任务需要大量的内存,过多的进程可能会导致内存不足。
- 对于 I/O 密集型任务,使用多进程可能并不是最佳选择,异步编程或多线程可能更合适。
三、多进程之间的通讯和数据共享
在 Python 多进程编程中,由于每个进程都有自己独立的内存空间,进程间的通讯(Inter - Process Communication,IPC)和数据共享就变得尤为重要。下面说明几种常见的进程间通讯和数据共享的方式:
1. Queue
(队列)
multiprocessing.Queue
是一个线程和进程安全的队列,可用于在多个进程间传递数据。
示例代码
import multiprocessing
def producer(queue):
# 生产者进程,向队列中放入数据
for i in range(5):
queue.put(i)
print(f"Produced {i}")
def consumer(queue):
# 消费者进程,从队列中取出数据
while True:
item = queue.get()
if item is None:
break
print(f"Consumed {item}")
if __name__ == '__main__':
# 创建一个队列
queue = multiprocessing.Queue()
# 创建生产者进程
p1 = multiprocessing.Process(target=producer, args=(queue,))
# 创建消费者进程
p2 = multiprocessing.Process(target=consumer, args=(queue,))
p1.start()
p2.start()
p1.join()
# 向队列中放入 None 作为结束信号
queue.put(None)
p2.join()
print("All processes finished.")
代码解释
multiprocessing.Queue()
:创建一个队列对象。queue.put(item)
:将数据item
放入队列。queue.get()
:从队列中取出数据。
2. Pipe
(管道)
multiprocessing.Pipe
返回一对连接对象,可用于在两个进程间进行双向通讯。
示例代码
import multiprocessing
def sender(conn):
# 发送进程,向管道发送数据
messages = ["Hello", "World", "!"]
for message in messages:
conn.send(message)
print(f"Sent: {message}")
conn.close()
def receiver(conn):
# 接收进程,从管道接收数据
while True:
try:
message = conn.recv()
print(f"Received: {message}")
except EOFError:
break
conn.close()
if __name__ == '__main__':
# 创建一个管道,返回两个连接对象
parent_conn, child_conn = multiprocessing.Pipe()
# 创建发送进程
p1 = multiprocessing.Process(target=sender, args=(child_conn,))
# 创建接收进程
p2 = multiprocessing.Process(target=receiver, args=(parent_conn,))
p1.start()
p2.start()
p1.join()
p2.join()
print("All processes finished.")
代码解释
multiprocessing.Pipe()
:创建一个管道,返回两个连接对象parent_conn
和child_conn
。conn.send(data)
:向管道发送数据。conn.recv()
:从管道接收数据。
3. Value
和 Array
multiprocessing.Value
和 multiprocessing.Array
可用于在多个进程间共享单个值或数组。
示例代码
import multiprocessing
def increment(counter):
# 增加共享值
for _ in range(1000):
with counter.get_lock():
counter.value += 1
if __name__ == '__main__':
# 创建一个共享的整数对象
counter = multiprocessing.Value('i', 0)
processes = []
# 创建多个进程
for _ in range(4):
p = multiprocessing.Process(target=increment, args=(counter,))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Final counter value: {counter.value}")
代码解释
multiprocessing.Value('i', 0)
:创建一个共享的整数对象,初始值为 0。'i'
表示整数类型。counter.get_lock()
:获取锁,确保在修改共享值时不会发生数据竞争。counter.value
:访问共享值。
4. Manager
multiprocessing.Manager
提供了一种更高级的方式来实现进程间的数据共享,它可以创建共享的列表、字典等对象。
示例代码
import multiprocessing
def worker(dictionary, key, value):
# 向共享字典中添加键值对
dictionary[key] = value
if __name__ == '__main__':
# 创建一个管理器对象
manager = multiprocessing.Manager()
# 创建一个共享的字典
shared_dict = manager.dict()
processes = []
# 创建多个进程
for i in range(5):
p = multiprocessing.Process(target=worker, args=(shared_dict, i, i * 2))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Shared dictionary: {shared_dict}")
代码解释
multiprocessing.Manager()
:创建一个管理器对象。manager.dict()
:创建一个共享的字典对象。- 可以像操作普通字典一样操作共享字典。
通过上述四种方式,你可以在 Python 多进程编程中实现进程间的通讯和数据共享。不同的方式适用于不同的场景,你可以根据具体需求选择合适的方法。
四、Python多进程方式的独特优势
1. 充分利用多核 CPU
在现代计算机系统中,CPU 通常具有多个核心。然而,由于 Python 的全局解释器锁(GIL)的存在,多线程在处理 CPU 密集型任务时并不能充分发挥多核 CPU 的性能优势。而 multiprocessing
模块可以创建多个独立的进程,每个进程都有自己独立的 Python 解释器实例和内存空间,它们可以并行地在不同的 CPU 核心上运行,从而充分利用多核 CPU 的计算能力,显著提高程序在 CPU 密集型任务上的执行效率。
2. 提高程序的稳定性
由于每个进程都是相互独立的,一个进程的崩溃不会影响其他进程的正常运行。如果在多线程编程中,某个线程出现了未处理的异常,可能会导致整个进程崩溃,从而影响其他线程的执行。而在多进程编程中,各个进程之间相互隔离,一个进程的异常只会导致该进程终止,其他进程可以继续正常工作,从而提高了程序的整体稳定性。
3. 数据安全性高
每个进程都有自己独立的内存空间,进程之间的数据是相互隔离的。这意味着在一个进程中对数据的修改不会影响其他进程中的数据,避免了多线程编程中常见的数据竞争和并发访问问题,提高了数据的安全性。当需要对数据进行并发处理时,只需将数据复制到各个进程中进行处理,处理完成后再进行汇总即可。
4. 适合处理复杂任务
multiprocessing
模块提供了丰富的进程管理和通信机制,如 Process
类用于创建和管理进程,Queue
、Pipe
用于进程间通信,Value
、Array
和 Manager
用于进程间数据共享等。这些功能使得 multiprocessing
非常适合处理复杂的任务,例如分布式计算、大规模数据处理等。可以将一个复杂的任务分解为多个子任务,每个子任务由一个独立的进程来处理,通过进程间的通信和协作完成整个任务。
5. 易于使用和集成
multiprocessing
模块的 API 设计简洁明了,易于学习和使用。它与 Python 的其他标准库和第三方库兼容性良好,可以方便地集成到现有的 Python 项目中。无论是小型脚本还是大型应用程序,都可以很容易地使用 multiprocessing
模块来实现多进程编程,提高程序的性能和并发处理能力。
五、多进程(multiprocessing)的最佳用途
multiprocessing
是 Python 标准库中用于实现多进程编程的模块,它在以下几种场景中能发挥最佳作用:
1. CPU 密集型任务
由于 Python 的全局解释器锁(GIL),多线程在处理 CPU 密集型任务时无法充分利用多核 CPU 的优势。而 multiprocessing
可以创建多个独立的进程,每个进程都有自己独立的 Python 解释器实例,能并行运行在不同的 CPU 核心上,显著提升计算效率。
适用场景举例
- 科学计算:如进行大规模的矩阵运算、数值模拟等。以矩阵乘法为例,对于大规模矩阵,计算量巨大,使用多进程可以将矩阵分割成多个子矩阵,每个进程负责计算一部分子矩阵的乘积,最后再合并结果。
- 图像和视频处理:像图像的滤镜处理、视频的编码解码等操作。例如在图像的卷积操作中,可以将图像分成多个区域,每个进程处理一个区域的卷积计算。
代码示例
import multiprocessing
def cpu_intensive_task(num):
result = 0
for i in range(1000000):
result += i
return result
if __name__ == '__main__':
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
tasks = [i for i in range(10)]
results = pool.map(cpu_intensive_task, tasks)
pool.close()
pool.join()
print("All CPU - intensive tasks are done.")
2. 并行处理大量独立任务
当有大量相互独立的任务需要处理时,使用 multiprocessing
可以并行执行这些任务,从而大大缩短整体处理时间。
适用场景举例
- 数据批量处理:例如对大量文件进行数据分析、转换或清洗。可以将文件列表分成多个部分,每个进程负责处理一部分文件。
- 网页爬虫:在爬取多个网站或大量网页时,每个网页的爬取任务可以看作是独立的,使用多进程可以同时发起多个爬取请求,提高爬取效率。
代码示例
import multiprocessing
def process_file(file_path):
# 模拟文件处理操作
print(f"Processing {file_path}")
return file_path
if __name__ == '__main__':
file_paths = [f"file_{i}.txt" for i in range(20)]
pool = multiprocessing.Pool(processes=4)
results = pool.map(process_file, file_paths)
pool.close()
pool.join()
print("All file processing tasks are done.")
3. 提高程序的稳定性和可靠性
由于每个进程都是相互独立的,一个进程的崩溃不会影响其他进程的正常运行。因此,在对程序稳定性要求较高的场景中,multiprocessing
是一个很好的选择。
适用场景举例
- 服务器端应用:例如 Web 服务器、数据库服务器等。当处理多个客户端请求时,每个请求可以由一个独立的进程来处理,这样即使某个请求处理过程中出现异常,也不会影响其他请求的处理。
- 长时间运行的后台任务:如监控系统、定时任务等。使用多进程可以确保某个任务的失败不会导致整个系统崩溃。
4. 复杂任务的并行分解
对于一些复杂的任务,可以将其分解为多个子任务,每个子任务由一个独立的进程来处理,通过进程间的通信和协作完成整个任务。
适用场景举例
- 分布式计算:在分布式系统中,将一个大的计算任务分解成多个小任务,分配给不同的进程或节点进行计算,最后汇总结果。
- 人工智能训练:在深度学习模型训练中,可以将数据分割成多个批次,每个进程负责训练一个批次的数据,加速训练过程。
代码示例(简单的任务分解)
import multiprocessing
def subtask(task_id, result_queue):
# 模拟子任务处理
result = task_id * 2
result_queue.put(result)
if __name__ == '__main__':
result_queue = multiprocessing.Queue()
processes = []
for i in range(5):
p = multiprocessing.Process(target=subtask, args=(i, result_queue))
processes.append(p)
p.start()
for p in processes:
p.join()
final_results = []
while not result_queue.empty():
final_results.append(result_queue.get())
print("Final results:", final_results)
综上所述,multiprocessing
在 CPU 密集型任务、并行处理大量独立任务、提高程序稳定性以及复杂任务的并行分解等方面具有显著优势,是 Python 中实现高效并发编程的重要工具。
六、多进程(multiprocessing)最不适宜的用途
multiprocessing
模块虽然在很多场景下非常有用,但也存在一些不太适宜使用的情况,以下是具体介绍:
1. 简单的 I/O 密集型任务
对于简单的 I/O 密集型任务,如少量的文件读写、简单的网络请求等,使用 multiprocessing
并不是一个好的选择,原因如下:
- 进程创建和销毁开销大:创建新进程需要分配系统资源,包括内存和 CPU 时间等。在简单的 I/O 密集型任务中,进程创建和销毁的开销可能会远远超过任务本身的执行时间,导致性能下降。
- 上下文切换开销:进程之间的上下文切换也需要消耗系统资源。对于简单的 I/O 操作,频繁的进程切换会增加额外的开销,降低程序的整体效率。
示例场景:一个脚本需要依次读取几个小文件的内容,使用单线程或多线程通常会比多进程更高效。
代码示例(不适合用多进程的情况):
import multiprocessing
def read_file(file_path):
with open(file_path, 'r') as f:
content = f.read()
return content
if __name__ == '__main__':
file_paths = ['file1.txt', 'file2.txt', 'file3.txt']
# 这里使用多进程处理简单文件读取,效率不高
pool = multiprocessing.Pool(processes=len(file_paths))
results = pool.map(read_file, file_paths)
pool.close()
pool.join()
print(results)
更好的做法是使用单线程:
def read_file(file_path):
with open(file_path, 'r') as f:
content = f.read()
return content
file_paths = ['file1.txt', 'file2.txt', 'file3.txt']
results = []
for file_path in file_paths:
results.append(read_file(file_path))
print(results)
2. 资源受限的环境
在资源受限的环境中,如内存有限的嵌入式设备或运行在资源紧张的服务器上,使用 multiprocessing
可能会导致系统资源耗尽,出现以下问题:
- 内存占用高:每个进程都有自己独立的内存空间,创建多个进程会占用大量的内存。如果系统内存不足,可能会导致程序崩溃或系统性能急剧下降。
- CPU 资源竞争:过多的进程会竞争 CPU 资源,导致系统负载过高,影响其他程序的正常运行。
3. 任务间高度依赖和频繁通信的场景
当任务之间存在高度依赖关系,需要频繁进行数据交换和同步时,multiprocessing
可能不是最佳选择,因为:
- 进程间通信成本高:进程间通信(IPC)需要通过特定的机制,如队列、管道等,这些机制的实现会带来一定的开销。频繁的 IPC 操作会降低程序的性能。
- 同步难度大:为了保证数据的一致性和正确性,需要进行复杂的同步操作,如加锁等。这会增加代码的复杂度,并且可能引入死锁等问题。
示例场景:一个任务需要等待另一个任务的中间结果才能继续执行,并且这种交互非常频繁,使用多线程或异步编程可能更合适。
4. 代码复杂度要求低的简单脚本
对于一些简单的脚本,开发者可能更注重代码的简洁性和易维护性。使用 multiprocessing
会引入进程管理、进程间通信等复杂的概念,增加代码的复杂度。在这种情况下,单线程或简单的多线程实现可能更符合需求。
示例:一个简单的脚本用于计算一组数字的总和,使用单线程代码会更简洁易懂:
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
print(total)
而使用多进程来实现会使代码变得复杂,且没有明显的性能提升。
七、笔者(老司机)的告诫
1、资源管理方面
- 合理分配进程数:别盲目多开进程,依任务类型和系统核心数设置,CPU 密集型接近物理核心数,I/O 密集型可多些,否则会因创建、切换开销大而拖慢性能。
- 关注内存占用:每个进程有独立内存空间,多进程会大量占内存,资源受限环境易致系统崩溃,要做好内存规划。
2、代码编写方面
- 遵守启动方法规则:不同操作系统启动进程方法有别,Windows 和 macOS 默认
spawn
,Linux 默认fork
,确保代码在不同系统兼容。 - 小心全局变量:各进程有独立内存,全局变量不共享,修改全局变量不会影响其他进程,别依赖全局变量通信。
3、进程间协作方面
- 通信要高效:进程通信靠队列、管道等,频繁通信开销大,尽量减少通信次数和数据量。
- 同步要谨慎:涉及共享资源访问,用锁机制同步,操作不当会引发死锁,加锁范围要最小化(能不用,就不用)。
4、调试和维护方面
- 调试有难度:多进程调试复杂,一个进程崩溃不影响其他,可多打日志,用单进程先调试逻辑。
- 代码可维护性:多进程增加代码复杂度,合理划分功能模块,写好注释,提高代码可读性和可维护性。