1. 进程(processing)
- 进程:进程是操作系统资源分配的最小单位,每个进程都有自己的内存空间。
- 线程:线程是进程中的执行单位,一个进程可以有多个线程。线程间共享进程的内存空间。
可以这么理解:
- 进程就像一个公司:每个公司都是独立的,有自己的一整套资源和预算(内存和CPU时间)。不同的公司之间是隔离的,即便它们都在做类似的业务,也不会直接共享资源。公司之间的沟通成本很高(因为要经过正式的邮件、合同等沟通手段)。
- 线程就像公司的员工:员工是公司中的“执行者”,他们可以直接访问和使用公司提供的资源(内存)。员工们可以快速交流(例如口头沟通),而不需要通过外部的正式渠道。
进程与线程的区别可以看后续文章。
为什么进程只能在__main__创建?
因为创建的子进程会自动导入目标函数所在的脚本,如果创建Proces的代码和目标函数的代码写在一起就会导入自己,而导入后,如果没有写在main,就会又创建一个Process,又去导入目标函数所在脚本,这样无限循环。
from multiprocessing import Process
def task(name):
print(f"Hello, {name}")
if __name__ == "__main__":
process1 = Process(target=task, args=("Process1",))
process2 = Process(target=task, args=("Process2",))
process1.start()
process2.start()
process1.join()
process2.join()
进程适用于计算密集型任务(要大量的CPU计算来完成,主要消耗处理器的算力。)
常见的计算密集型任务包括:
- 图像处理:例如图像滤波、图像特征提取、大量图片的批量处理和转换。
- 视频编码和解码:视频的编码、解码和格式转换,通常需要处理大量的图像帧。
- 科学计算和数值运算:例如大规模矩阵运算、线性代数计算、求解微分方程等。
- 机器学习和深度学习模型训练:模型训练(尤其是大数据量的深度学习训练)涉及大量矩阵运算和反向传播计算。
- 加密和解密:复杂的加密算法计算,如AES加密、哈希运算等。
- 模拟和仿真:例如物理引擎模拟、3D渲染、蒙特卡洛模拟等。
2. 多进程(Multiprocessing)
概念
多进程是指在程序中启动多个独立的进程,每个进程都有自己的内存空间和系统资源,它们彼此独立运行。多进程模式的实现方式是让操作系统来管理不同的进程并将它们分配到不同的CPU核心上执行,因此可以实现真正的并行处理。
特点
- 独立性:每个进程有独立的内存空间,不共享数据。因此,一个进程崩溃不会影响其他进程的运行。
- 并行能力:由于进程是完全独立的,操作系统可以将进程分配到不同的CPU核心上,实现真正的并行计算,非常适合CPU密集型任务。
- 资源开销较大:创建和切换进程的开销较大,因为每个进程都需要分配独立的内存空间和资源。
应用场景
- CPU密集型任务:如图像处理、大量的科学计算、加密解密操作等。
- 高可靠性任务:由于进程隔离,如果一个进程崩溃,不会影响其他进程的执行,因此在高可靠性需求的场景下,多进程更合适。
1. 基本的进程创建
from multiprocessing import Process
def task(name):
print(f'Hello, {name}')
if __name__ == '__main__':
process = Process(target=task, args=('World',)) # 创建进程
process.start() # 启动进程
process.join() # 等待进程结束,join() 方法用于等待一个进程完成执行后再继续执行主程序或其他进
#程。它确保主进程在子进程执行完毕之后再继续执行后续的操作。
2. 使用进程池(Pool)
multiprocessing.Pool
类用于创建一个进程池,可以用来管理多个进程。它能让我们限制并发的进程数量,便于管理并提高效率。常用方法有apply
、apply_async
、map
和map_async
。
from multiprocessing import Pool
def square(x):
return x * x
if __name__ == '__main__':
with Pool(4) as pool: # 创建一个包含4个进程的进程池
results = pool.map(square, [1, 2, 3, 4, 5]) # 使用map并行计算
print(results) # 输出:[1, 4, 9, 16, 25]
具体用法可以查看pool文章
3. 进程间通信
multiprocessing
模块提供了几种进程间的通信方式,包括Queue
、Pipe
和Manager
。
(1)使用Queue
Queue
是线程和进程安全的队列,可以在不同的进程之间传递数据。
from multiprocessing import Process, Queue
def producer(queue):
queue.put("Data from producer")
def consumer(queue):
data = queue.get()
print(data)
if __name__ == '__main__':
queue = Queue()
p1 = Process(target=producer, args=(queue,))
p2 = Process(target=consumer, args=(queue,))
p1.start()
p2.start()
p1.join()
p2.join()
def producer(queue):
data = ("参数1", "参数2", 123)
queue.put(data) # 将元组作为一个整体放入队列
def consumer(queue):
data = queue.get() # 从队列中获取元组
print(f"消费者接收到多个参数:{data}")
if __name__ == "__main__":
queue = Queue()
p1 = Process(target=producer, args=(queue,))
p2 = Process(target=consumer, args=(queue,))
p1.start()
p2.start()
p1.join()
p2.join()
######## ###############
queue遵循先入先出,put是入,get是出
代码解释
Queue()
:Queue
是一个线程安全的队列,用于进程间的通信。它可以用来存储由生产者进程产生的数据,并由消费者进程进行消费。event.set() 作用:将事件的状态设置为 "set"(即,标志位为 True)。一旦调用了
set()
,所有正在等待该事件的进程将被唤醒。 用法:通常由生产者进程调用,通知消费者进程某个事件发生,或某个条件满足。event.wait()
- 作用:阻塞当前进程,直到事件的状态为 "set"。如果事件的状态已经是 "set",则
wait()
立即返回;如果事件尚未设置为 "set",则当前进程会一直阻塞,直到其他进程调用set()
来设置事件状态。- 用法:通常由消费者进程调用,等待生产者进程通知它可以开始处理数据。
4.其他常用方法:
clear()
:
- 作用:将事件的状态重置为 "未设置"(即,标志位为 False)。当事件被清除后,所有等待该事件的进程将继续阻塞,直到
set()
被再次调用。- 用法:可以在某些情况下,重置事件状态,重新开始等待。
- 即使已经在wait()之后,若是遇到了其他进程的event.clear()依旧会阻塞,等待set(),共饮event的进程会相互影响。
is_set()
:
- 作用:检查事件的状态,返回
True
表示事件已被设置,返回False
表示事件未被设置。- 用法:可以用来检查事件状态,而不必阻塞进程。
(2)使用Pipe
Pipe
创建一个双向通信的管道,返回一个包含两个连接对象的元组。一个连接对象用于发送,另一个用于接收。
from multiprocessing import Process, Pipe
import time
def sender(conn):
"""发送者进程,发送两条消息后关闭管道"""
conn.send("Hello from sender!")
time.sleep(1)
conn.send("Another message from sender!")
conn.close() # 关闭连接,表示不再发送数据
def receiver(conn):
"""接收者进程,不断接收消息直到管道关闭"""
while True:
if conn.poll(): # 检查管道中是否有数据
message = conn.recv() # 如果有数据,读取
print(f"Receiver received: {message}")
else:
# 如果没有数据,退出循环
print("Receiver: No more data in the pipe, exiting...")
break
if __name__ == "__main__":
parent_conn, child_conn = Pipe()
# 创建发送者和接收者进程
sender_process = Process(target=sender, args=(child_conn,))
receiver_process = Process(target=receiver, args=(parent_conn,))
# 启动进程
sender_process.start()
receiver_process.start()
# 等待进程完成
sender_process.join()
receiver_process.join()
parent_conn, child_conn = Pipe()
创建了一对连接对象。每个对象都可以发送和接收数据,但通常将它们分配给不同的进程,以便实现双向通信。
poll()
方法检查管道是否有数据
- 数据会在被读取后从管道中移除,无法再次读取相同的数据。
- 如果数据没有被及时读取,它将保持在管道中,直到被读取或管道关闭。
- 管道是单向的,数据从发送端流向接收端,无法在管道中返回。
- 如果没有数据发生了,记得要执行close()
(3)使用Manager
Manager
提供了多种共享数据类型,例如list
、dict
等,允许多个进程安全地访问和修改这些数据。
from multiprocessing import Process, Manager
def worker(shared_dict, key, value):
shared_dict[key] = value
if __name__ == '__main__':
with Manager() as manager:
shared_dict = manager.dict() # 创建一个共享字典
processes = [Process(target=worker, args=(shared_dict, i, i*2)) for i in range(5)]
for p in processes:
p.start()
for p in processes:
p.join()
print(shared_dict) # 输出:{0: 0, 1: 2, 2: 4, 3: 6, 4: 8}
4. 进程同步
multiprocessing
模块提供了多种同步机制,例如Lock
、RLock
、Semaphore
等,来确保进程间不会产生资源竞争。
(1)使用Lock
Lock
可以确保同一时间只有一个进程可以访问临界资源
from multiprocessing import Process, Lock
def task(lock, num):
with lock:
print(f'Process {num} is running')
if __name__ == '__main__':
lock = Lock()
processes = [Process(target=task, args=(lock, i)) for i in range(5)]
for p in processes:
p.start()
for p in processes:
p.join()
(2)使用Semaphore
Semaphore
允许多个进程并发访问资源,但可以设置并发的上限。
from multiprocessing import Process, Semaphore
import time
def task(sem, num):
sem.acquire() # 获取信号量
print(f'Process {num} is running')
time.sleep(1)
sem.release() # 释放信号量
if __name__ == '__main__':
sem = Semaphore(2) # 最多允许2个进程同时运行
processes = [Process(target=task, args=(sem, i)) for i in range(5)]
for p in processes:
p.start()
for p in processes:
p.join()
- 计数器:
Semaphore
内部维护一个计数器,表示当前可用的资源数量。每当一个进程或线程获得资源时,计数器减 1;当资源释放时,计数器加 1。 - 阻塞:如果计数器为 0,意味着没有可用资源,此时任何请求资源的进程或线程都会被阻塞,直到其他进程或线程释放资源。
- 释放资源:当一个进程或线程完成工作后,它需要释放资源,
Semaphore
会增加计数器的值,允许其他被阻塞的进程或线程获取资源。 -
Semaphore
的常用方法: acquire()
:申请资源,计数器减 1。如果计数器为 0,调用会阻塞,直到有可用资源。release()
:释放资源,计数器加 1。调用后,可能会唤醒一个等待的进程或线程。
5. 使用Value
和Array
共享内存
如果需要共享简单的数值或数组,可以使用multiprocessing.Value
和multiprocessing.Array
。
from multiprocessing import Process, Value, Array
def increment(shared_num, shared_arr):
with shared_num.get_lock(): # 使用锁保护共享变量
shared_num.value += 1
for i in range(len(shared_arr)):
shared_arr[i] += 1
if __name__ == '__main__':
shared_num = Value('i', 0) # 'i' 表示整数类型
shared_arr = Array('i', [1, 2, 3])
processes = [Process(target=increment, args=(shared_num, shared_arr)) for _ in range(3)]
for p in processes:
p.start()
for p in processes:
p.join()
print(shared_num.value) # 输出 3
print(shared_arr[:]) # 输出 [4, 5, 6]
Value
需要显式的锁机制,因为它是单个共享数据项,多个进程同时访问可能会引发竞态条件。Array
内部实现了原子操作,通常不需要显式的锁来同步对单个元素的访问。然而,如果多个进程同时修改Array
中的多个元素,可能还是需要显式加锁来防止出现竞态条件。