一、多进程概念
多线程就像是在一个办公室里让多个员工同时工作,但由于 Python 的全局解释器锁(GIL),多个线程在同一时间实际上只能有一个线程执行 Python 字节码,这在处理 CPU 密集型任务时会受到限制。而多进程则是开启多个独立的 “办公室”(进程),每个进程都有自己独立的 Python 解释器和内存空间,能真正实现并行计算,适合处理 CPU 密集型任务。
二、基础语法
Python的os模块提供了fork()
函数。由于Windows系统没有fork()
调用,因此要实现跨平台的多进程编程,可以使用multiprocessing模块的Process
类来创建子进程,而且该模块还提供了更高级的封装,例如批量启动进程的进程池(Pool
)、用于进程间通信的队列(Queue
)和管道(Pipe
)等
import multiprocessing
def 进程任务(任务编号):
print(f"进程 {任务编号} 开始执行")
import time
time.sleep(2)
print(f"进程 {任务编号} 执行完成")
if __name__ == '__main__':
进程列表 = []
for i in range(3):
进程 = multiprocessing.Process(target=进程任务, args=(i,))
进程列表.append(进程)
进程.start()
for 进程 in 进程列表:
进程.join()
print("所有进程执行完成")
代码解释
- 定义任务函数:
进程任务
函数代表每个进程要执行的任务,使用time.sleep
模拟耗时操作。 - 创建进程对象:通过
multiprocessing.Process
创建进程对象,指定要执行的任务函数和参数。 - 启动进程:调用
start()
方法启动进程。 - 等待进程结束:使用
join()
方法确保主进程等待所有子进程执行完毕后再继续执行后续代码。需要注意的是,在 Windows 和一些其他操作系统中,多进程代码必须放在if __name__ == '__main__':
语句块中,以避免递归创建进程。
三、进程间通信(IPC)
在多个 进程并行工作时,有时需要进行信息传递,这就涉及到进程间通信(IPC)。Python 的multiprocessing
模块提供了多种方式实现 IPC,如Queue
(队列)和Pipe
(管道)。
3.1 使用Queue
进行进程间通信
import multiprocessing
def 生产者(队列):
for i in range(5):
print(f"生产者生产了 {i}")
队列.put(i)
def 消费者(队列):
while True:
数据 = 队列.get()
if 数据 is None:
break
print(f"消费者消费了 {数据}")
if __name__ == '__main__':
队列 = multiprocessing.Queue()
生产者进程 = multiprocessing.Process(target=生产者, args=(队列,))
消费者进程 = multiprocessing.Process(target=消费者, args=(队列,))
生产者进程.start()
消费者进程.start()
生产者进程.join()
队列.put(None)
消费者进程.join()
print("所有任务完成")
代码解释
- 定义生产者和消费者函数:
生产者
函数将数据放入队列,消费者
函数从队列中取出数据进行处理。 - 创建队列和进程:使用
multiprocessing.Queue
创建队列,创建生产者和消费者进程,并将队列作为参数传递给它们。 - 启动进程:调用
start()
方法启动进程。 - 结束进程:生产者进程结束后,向队列中放入
None
作为结束标志,消费者进程接收到None
后退出循环,最后等待所有进程结束。
3.2 使用Pipe进行进程间通信
Pipe
可以创建一对连接对象,这对对象分别代表管道的两端,在不同的进程中可以通过这两端进行数据的发送和接收
import multiprocessing
def sender(conn):
"""
发送数据的进程函数
:param conn: 管道的发送端
"""
messages = ["Hello", "World", "Python"]
for message in messages:
print(f"发送消息: {message}")
conn.send(message)
# 模拟发送数据的耗时操作
import time
time.sleep(1)
conn.close()
def receiver(conn):
"""
接收数据的进程函数
:param conn: 管道的接收端
"""
while True:
try:
message = conn.recv()
if not message:
break
print(f"接收到消息: {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("所有进程执行完毕")
代码解释
- 导入模块:导入
multiprocessing
模块,它提供了多进程编程的功能。 - 定义发送函数
sender
:- 该函数接收一个管道连接对象
conn
作为参数。 - 定义一个消息列表
messages
,并遍历该列表,依次将消息发送到管道中。 - 使用
conn.send(message)
方法发送消息,time.sleep(1)
模拟发送数据的耗时操作。 - 最后关闭连接
conn.close()
。
- 该函数接收一个管道连接对象
- 定义接收函数
receiver
:- 该函数也接收一个管道连接对象
conn
作为参数。 - 使用
while True
循环不断从管道中接收消息,使用conn.recv()
方法接收消息。 - 如果接收到的消息为空,则跳出循环;如果遇到
EOFError
异常,也跳出循环。 - 最后关闭连接
conn.close()
。
- 该函数也接收一个管道连接对象
- 主程序:
- 使用
multiprocessing.Pipe()
创建一个管道,返回两个连接对象parent_conn
和child_conn
。 - 创建两个进程
p1
和p2
,分别执行sender
和receiver
函数,并将相应的管道连接对象作为参数传递给它们。 - 启动这两个进程,使用
start()
方法。 - 等待两个进程执行完毕,使用
join()
方法。 - 最后打印 “所有进程执行完毕”。
- 使用
四、进程间同步
在多进程编程中,当多个进程需要访问共享资源时,为了避免数据不一致或其他并发问题,就需要进行进程间同步。Python 的 multiprocessing
模块提供了多种进程间同步的机制
4.1 锁(Lock
)
原理
锁是一种最基本的同步机制,同一时间只允许一个进程获取锁并访问共享资源,其他进程需要等待锁被释放后才能获取锁。这样可以确保在任何时刻只有一个进程对共享资源进行操作,避免数据竞争。
错误使用共享资源的示例代码
import multiprocessing
# 共享资源
shared_variable = 0
# 创建锁对象
lock = multiprocessing.Lock()
def increment():
global shared_variable
for _ in range(100000):
# 获取锁
lock.acquire()
try:
shared_variable += 1
finally:
# 释放锁
lock.release()
if __name__ == '__main__':
processes = []
for _ in range(2):
p = multiprocessing.Process(target=increment)
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"最终结果: {shared_variable}")
该示例运行的结果为最终结果: 0,,为啥会这样子?,如果我们按照前面线程的方式修改一下,变成这样:
import multiprocessing
import threading
# 共享资源
shared_variable = 0
# 创建锁对象
lock = multiprocessing.Lock()
def increment():
global shared_variable
for _ in range(100000):
# 获取锁
lock.acquire()
try:
shared_variable += 1
finally:
# 释放锁
lock.release()
if __name__ == '__main__':
processes = []
for _ in range(2):
# p = multiprocessing.Process(target=increment)
p = threading.Thread(target=increment)
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"最终结果: {shared_variable}")
结果就对了。为什么?
原因:线程会共享父进程的共享空间,所以就可以;而进程是有自己独立空间的。
在 Python 的 multiprocessing 模块中,每个子进程都有自己独立的内存空间,父进程中的全局变量 shared_variable 并不会被子进程直接共享。子进程对 shared_variable 的修改只会影响其自身的内存副本,而不会影响父进程或其他子进程的副本。
我们可以使用multiprocessing 提供的共享内存机制(如 Value 或 Array):
import multiprocessing
# 共享资源
# 使用共享内存
shared_variable = multiprocessing.Value('i', 0) # 'i' 表示整数类型,初始值为 0
# 创建锁对象
lock = multiprocessing.Lock()
def increment(shared_variable, lock):
for _ in range(100000):
# 获取锁
lock.acquire()
try:
shared_variable.value += 1
finally:
# 释放锁
lock.release()
if __name__ == '__main__':
processes = []
for _ in range(2):
p = multiprocessing.Process(target=increment, args=(shared_variable,lock))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"最终结果: {shared_variable.value}")
注意:锁对象lock和共享变量是在全局作用域创建的,如果不传进去给子进程,就会导致子进程可能各自拥有自己的锁和共享变量,无法正确同步对共享变量的访问。
所以要将锁对象lock和共享变量作为参数传递给子进程。
代码解释
lock = multiprocessing.Lock()
:创建一个锁对象。lock.acquire()
:获取锁,如果锁已经被其他进程获取,当前进程会阻塞等待。lock.release()
:释放锁,允许其他进程获取锁。try...finally
语句确保即使在操作过程中出现异常,锁也能被正确释放,避免死锁。
4.2 信号量(Semaphore
)
原理
信号量是一种更灵活的同步机制,它可以控制同时访问共享资源的进程数量。信号量内部维护一个计数器,当进程请求访问资源时,计数器减 1;当进程释放资源时,计数器加 1。当计数器为 0 时,其他进程需要等待。
import multiprocessing
import time
# 信号量,允许同时有 2 个进程访问
semaphore = multiprocessing.Semaphore(2) # 可以试试修改为1,方便观察程序运行
def worker(process_id, semaphore):
print(f"进程 {process_id} 正在等待信号量")
# 获取信号量
semaphore.acquire()
try:
print(f"进程 {process_id} 已获取信号量,开始工作")
time.sleep(2)
finally:
# 释放信号量
print(f"进程 {process_id} 释放信号量")
semaphore.release()
if __name__ == '__main__':
processes = []
for i in range(5):
p = multiprocessing.Process(target=worker, args=(i,semaphore))
processes.append(p)
p.start()
for p in processes:
p.join()
代码解释
semaphore = multiprocessing.Semaphore(2)
:创建一个信号量对象,初始值为 2,表示允许同时有 2 个进程访问共享资源。semaphore.acquire()
:获取信号量,如果信号量计数器大于 0,则计数器减 1,进程继续执行;否则,进程阻塞等待。semaphore.release()
:释放信号量,计数器加 1。
4.3 事件(Event
)
原理
事件是一种简单的同步机制,它可以让一个进程等待另一个进程发出的信号。事件对象内部有一个标志位,进程可以通过 set()
方法将标志位设置为 True
,通过 clear()
方法将标志位设置为 False
,其他进程可以通过 wait()
方法等待标志位变为 True
。
import multiprocessing
import time
# 创建事件对象
event = multiprocessing.Event()
def waiter(event):
print("等待事件被设置...")
# 等待事件被设置
event.wait()
print("事件已被设置,开始工作")
def setter(event):
print("正在执行一些操作...")
time.sleep(2)
# 设置事件
event.set()
print("事件已设置")
if __name__ == '__main__':
p1 = multiprocessing.Process(target=waiter, args=(event,))
p2 = multiprocessing.Process(target=setter, args=(event,))
p1.start()
p2.start()
p1.join()
p2.join()
代码解释
event = multiprocessing.Event()
:创建一个事件对象。event.wait()
:等待事件的标志位变为True
,如果标志位已经是True
,则立即返回;否则,进程阻塞等待。event.set()
:将事件的标志位设置为True
,唤醒所有等待该事件的进程。event.clear()
:将事件的标志位设置为False
。
4.4 条件变量(Condition
)
原理
条件变量是一种更高级的同步机制,它结合了锁和事件的功能。条件变量允许进程在满足特定条件时进行等待,当条件满足时,其他进程可以唤醒等待的进程。
import multiprocessing
import time
# 创建条件变量
condition = multiprocessing.Condition()
# 共享资源
shared_variable = multiprocessing.Value('i', 0) # 'i' 表示整数类型,初始值为 0
def producer(shared_variable, condition):
with condition:
print("生产者开始生产...")
time.sleep(2)
shared_variable.value = 1
print("生产者生产完成,通知消费者")
# 通知等待的消费者
condition.notify()
def consumer(shared_variable, condition):
with condition:
print("消费者等待生产完成...")
# 等待生产者生产完成
condition.wait()
print(f"消费者开始消费,共享变量的值为: {shared_variable.value}")
if __name__ == '__main__':
p1 = multiprocessing.Process(target=producer, args=(shared_variable, condition))
p2 = multiprocessing.Process(target=consumer, args=(shared_variable, condition))
p2.start()
p1.start()
p1.join()
p2.join()
代码解释
condition = multiprocessing.Condition()
:创建一个条件变量对象。condition.wait()
:释放锁并阻塞当前进程,直到其他进程调用condition.notify()
或condition.notify_all()
唤醒它。condition.notify()
:唤醒一个等待在该条件变量上的进程。condition.notify_all()
:唤醒所有等待在该条件变量上的进程。with condition
:使用上下文管理器来自动获取和释放锁。
五、进程池
使用multiprocessing.Pool来创建进程池
import multiprocessing
def 处理任务(任务编号):
print(f"开始处理任务 {任务编号}")
import time
time.sleep(2)
print(f"任务 {任务编号} 处理完成")
if __name__ == '__main__':
with multiprocessing.Pool(processes=3) as 进程池:
任务列表 = [1, 2, 3, 4, 5]
进程池.map(处理任务, 任务列表)
print("所有任务完成")
代码解释
- 创建进程池:使用
multiprocessing.Pool
创建一个包含 3 个进程的进程池,with
语句会在代码块结束时自动关闭进程池。 - 提交任务到进程池:使用
map()
方法将任务列表中的每个任务分配给进程池中的进程进行处理。 - 等待任务完成:
map()
方法会阻塞主进程,直到所有任务都处理完成。
六、多线程与多进程的对比
6.1 适用场景
- 多线程:适用于 I/O 密集型任务,如网络请求、文件读写等。因为在这些任务中,线程大部分时间都在等待 I/O 操作完成,此时可以让其他线程继续执行,充分利用 CPU 时间。
- 多进程:适用于 CPU 密集型任务,如大规模数据计算、图像处理等。由于每个进程都有自己独立的 Python 解释器和内存空间,能真正实现并行计算,充分利用多核 CPU 的性能。
6.2 优缺点
多线程:
- 优点:线程创建和销毁的开销较小,线程间通信和数据共享比较方便,适合处理 I/O 密集型任务时提高程序的响应性。
- 缺点:受 Python 全局解释器锁(GIL)的限制,多个线程在同一时间只能有一个线程执行 Python 字节码,在处理 CPU 密集型任务时效率不高;线程同步和资源管理比较复杂,容易出现死锁等问题。
多进程
- 优点:能真正实现并行计算,充分利用多核 CPU 的性能,适合处理 CPU 密集型任务;每个进程都有自己独立的内存空间,避免了多线程中的资源竞争问题。
- 缺点:进程创建和销毁的开销较大,进程间通信和数据共享相对复杂,需要使用专门的 IPC 机制。
七、应用场景示例
import multiprocessing
import threading
import time
import requests
# 模拟文件内容提取(CPU 密集型操作)
def extract_content(file_path):
print(f"开始提取文件 {file_path} 的内容")
# 模拟耗时的内容提取操作
time.sleep(2)
content = f"文件 {file_path} 的提取内容"
print(f"完成文件 {file_path} 的内容提取")
return content
# 模拟网络请求(I/O 密集型操作)
def send_request(content):
print(f"开始发送内容:{content} 到服务器")
try:
# 模拟网络请求
response = requests.get("https://www.example.com")
if response.status_code == 200:
print(f"内容 {content} 发送成功")
else:
print(f"内容 {content} 发送失败,状态码:{response.status_code}")
except Exception as e:
print(f"内容 {content} 发送时出现错误:{e}")
# 处理单个文件的函数,结合多线程发送内容
def process_file(file_path):
content = extract_content(file_path)
# 创建多个线程发送内容
threads = []
for _ in range(3):
thread = threading.Thread(target=send_request, args=(content,))
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
if __name__ == '__main__':
# 模拟多个文件
file_paths = [f"file_{i}.txt" for i in range(3)]
# 创建多个进程处理不同的文件
processes = []
for file_path in file_paths:
process = multiprocessing.Process(target=process_file, args=(file_path,))
processes.append(process)
process.start()
# 等待所有进程完成
for process in processes:
process.join()
print("所有文件处理完成")
代码解释
extract_content
函数:模拟文件内容提取的 CPU 密集型操作,使用time.sleep(2)
模拟耗时操作。send_request
函数:模拟网络请求的 I/O 密集型操作,使用requests.get
发送请求,并根据响应状态码判断请求是否成功。process_file
函数:对单个文件进行处理,先调用extract_content
函数提取文件内容,然后创建多个线程并行发送提取的内容。- 主程序:
- 定义一个包含多个文件路径的列表
file_paths
。 - 创建多个进程,每个进程处理一个文件,调用
process_file
函数。 - 启动所有进程,并等待它们完成。
- 定义一个包含多个文件路径的列表