1 并发编程
并发编程指的是在一个程序中同时执行多个独立的任务(或操作),使得这些任务看起来是同时执行的。并发编程可以提高程序的效率、性能及响应速度。并发编程有多种实现方式,这里仅介绍多线程和多进程。
- 多进程:多进程是指在操作系统中同时运行多个独立的进程。每个进程都有自己独立的内存空间、代码段、数据段、堆栈等资源,是程序的执行实例。可以同时执行多个任务,并且每个进程之间相互独立,互不干扰。
- 多线程:多线程编程是一种计算机编程技术,它允许在一个程序中同时执行多个线程(Thread)。多线程共享之间共享同一个进程的资源,比如内存、计算资源等。
本篇博客重点介绍多进程。
2 多进程
在Python中,可以使用multiprocessing
模块实现多进程编程,该模块可以创建多个进程,并为每个进程创建Python解释器和全局解释锁(Global Interpreter Lock,GIL),这样能充分利用多核处理器的能力,实现真正的并行执行。
2.1 创建和管理进程
在multiprocessing
模块中可以使用Process
类创建并管理进程。其用法举例如下:
import multiprocessing
import time
import datetime
def worker(num):
print(f"第{num}个子进程开始.",datetime.datetime.now())
time.sleep(5)
print(f"第{num}个子进程结束.",datetime.datetime.now())
if __name__=="__main__": #__name__=="__main__"是必须的,否则代码报错
print("主进程开始.",datetime.datetime.now())
processes=[]
for i in range(3):
p=multiprocessing.Process(target=worker,args=(i,))
processes.append(p)
p.start() #启动进程
for p in processes:
p.join() #等待进程结束
print("主进程结束.",datetime.datetime.now())
其执行结果如下:
主进程开始. 2024-05-21 16:03:50.406936
第0个子进程开始. 2024-05-21 16:03:50.470722
第0个子进程结束. 2024-05-21 16:03:55.473361
第1个子进程开始. 2024-05-21 16:03:50.472558
第1个子进程结束. 2024-05-21 16:03:55.477338
第2个子进程开始. 2024-05-21 16:03:50.474297
第2个子进程结束. 2024-05-21 16:03:55.477366
主进程结束. 2024-05-21 16:03:55.496410
从代码运行结果中可以看到,三个子进程几乎同时开始,同时结束。
虽然可以在代码中创建任意数量的进程,但实际能够并行执行的进程数量取决于本地处理器的物理核心和逻辑核心数。(但由于本机启动了超线程技术,这部分效果没有运行出来。)
另外,multiprocessing
模块中的Pool
(进程池)类可以高效的管理多个进程。使用该方法,用户不再需要手动管理各个进程。用法举例如下:
from multiprocessing import Pool
def square(x):
return x * x
if __name__ == '__main__':
with Pool(4) as pool: # 创建包含4个进程的进程池
results = pool.map(square, range(10)) # 并行执行任务
print(results)
with Pool(4) as pool:
results = pool.apply(square,(10,)) #只能传递一个参数
print(results)
with Pool(4) as pool:
results = pool.map_async(square, range(10))
results.wait()
print(results.get())
代码运行结果如下:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
100
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
2.2 通信机制
进程通信主要用于在不同进程之间传递数据和消息。在multiprocessing
模块中,通常使用消息传递机制进行进程之间的通信(尽量避免使用锁这类同步原语)。在该模块中,两个进程之间的通信可以使用Pipe
类,而多个进程之间(比如多个生产者多个消费者)可以使用Queue
类。这两者的主要区别如下:
Pipe
会在通信的两个进程之间建立一个管道,这个管道可以是双向的(或单向的),适用于两个进程之间点对点通信;
from multiprocessing import Process, Pipe
def sender(conn):
messages = ["Hello", "World", "from", "sender"]
for msg in messages:
conn.send(msg)
print(f"Sender sent: {msg}",flush=True)
conn.close()
def receiver(conn):
while True:
try:
msg = conn.recv()
#不设置flush=True看不到print语句的输出结果
print(f"Receiver received: {msg}",flush=True)
except EOFError:
print("Receiver received EOF",flush=True)
break
if __name__ == "__main__":
parent_conn, child_conn = Pipe()
p1 = Process(target=sender, args=(parent_conn,))
p2 = Process(target=receiver, args=(child_conn,))
p1.start()
p2.start()
p1.join()
p2.join(timeout=20)#等待20秒,超时后不再等待
print("Main process ends")
其执行结果如下:
Sender sent: Hello
Sender sent: World
Sender sent: from
Sender sent: sender
Receiver received: Hello
Receiver received: World
Receiver received: from
Receiver received: sender
Main process ends
Queue
会创建一个队列,这个队列允许任意数量的进程添加数据,并允许任意数量的进程从队列中取出数据,所以比较适合多生产者-多消费者的情况。
from multiprocessing import Process, Queue
import time
import random
def producer(queue, producer_id):
for i in range(3):
item = f"item {i} from producer {producer_id}"
queue.put(item) # 生产者将item放入队列
print(f"Producer {producer_id} produced: {item}",flush=True)
time.sleep(random.random()) # 模拟生产时间
def consumer(queue, consumer_id):
while True:
item = queue.get() # 消费者从队列中取出item
if item is None: # 使用 None 作为结束信号
break
print(f"Consumer {consumer_id} consumed: {item}",flush=True)
time.sleep(random.random()) # 模拟消费时间
if __name__ == "__main__":
queue = Queue()
# 启动多个生产者
producers = []
for i in range(2):
p = Process(target=producer, args=(queue, i))
producers.append(p)
p.start()
# 启动多个消费者
consumers = []
for i in range(3):
c = Process(target=consumer, args=(queue, i))
consumers.append(c)
c.start()
# 等待所有生产者进程完成
for p in producers:
p.join()
#需要向每一个消费者发送结束信号,否则未收到结束信号的消费者会一直等待
for _ in range(3):
queue.put(None)
# 等待所有消费者进程完成
for c in consumers:
c.join()
print("All producers and consumers have finished execution.")
其代码运行结果如下:
Producer 0 produced: item 0 from producer 0
Producer 1 produced: item 0 from producer 1
Consumer 0 consumed: item 0 from producer 0
Consumer 2 consumed: item 0 from producer 1
Producer 0 produced: item 1 from producer 0
Consumer 1 consumed: item 1 from producer 0
Producer 1 produced: item 1 from producer 1
Consumer 0 consumed: item 1 from producer 1
Producer 0 produced: item 2 from producer 0
Consumer 2 consumed: item 2 from producer 0
Producer 1 produced: item 2 from producer 1
Consumer 1 consumed: item 2 from producer 1
All producers and consumers have finished execution.