29. 多进程编程
一、什么是进程
进程(process)则是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈以及其它用于跟踪执行的辅助数据。操作系统管理其上所有进程的执行,并为这些进程合理分配时间。进程也可以通过派生新的进程来执行其它任务,不过因为每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信的方式共享数据;
二、进程的生命周期
一个完整进程的生命周期中通常要经过如下的五种状态:
- 创建:当一个 Process 类或及其子类的对象被声明并创建时,新生的进程就处于创建状态;
- 就绪:处于新建的进程被 start() 后,将进入进程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没分配到 CPU 资源;
- 运行:当就绪的进程被调度并获得 CPU 资源时,便进入运行状态,run() 方法定义了进程的操作和功能;
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态;
- 退出:进程完成了它的全部或进程被提前强制性中止或出现异常导致结束;

三、进程的创建
【1】、使用 multiprocessing 模块
如果我们想要执行一个单独的任务,那么就需要创建一个新的进程。在 Python 中,我们可以使用 multiprocessing 模块中的 Process 类创建一个对象。这个对象表示一个进程,但它不会真正创建出来一个进程。而当我们调用 start() 方法时,才会真正创建一个新的子进程,并开始执行的。
Process(
group = None,
target = None, # 子进程要执行的可调用对象
name = None, # 子进程名,如果不传,默认为Process-N,N为进程编号
args = (), # 给target传递的位置参数
kwargs = {}, # 给target传递的关键字参数
*,
daemon = None # 子进程是否是守护进程
)
至于这个进程去执行哪里的代码,要看在用 Process 创建对象的时候给 target 传递的是哪个函数的引用,即将来进程就会执行 target 参数指向的那个函数。target 指向的那个函数代码执行完之后,意味着这个子进程结束;
创建 Process 对象时,target 参数指明进程将来去哪里执行代码,而 args 参数执行进程去执行代码时所携带的数据,并且 args 参数是一个元组。如果我们想给指定的参数传递数据,我们可以给 kwargs 参数传递一个字典。
import time
from multiprocessing import Process
def task(name):
print(f"{name}开始执行")
time.sleep(3)
print(f"{name}执行结束")
"""
Window操作系统下,创建进程一定要在main内创建
因为Window系统下创建进程类似于模块的导入的方式,会从上往下依次执行代码
Linux中则是直接将代码完成拷贝一份
"""
if __name__ == "__main__":
# 1、实例化对象
p1 = Process(target=task, args=("进程1",))
p2 = Process(target=task, kwargs={"name": "进程2"})
# 2、开启进程
p1.start() # 告诉操作系统帮你创建一个进程
p2.start()
print("主进程执行")
在 Windows 系统下,如果我们不把创建进程的代码放在 main 内,当执行文件的主线程执行到创建 p1 进程时,传入 target 传入传入 task 函数,会导致 Python 解释器再开一个进程找到 task 函数在哪个文件中定义的,而导入模块会从上往下依次执行代码,会再次执行到创建 p1 进程,从而再开一个进程找到 task 函数在哪个文件中定义的,然后会以此循环下去。
【2】、自定义类继承 Process
我们可以自定义一个类继承 Process,然后一定要实现它的 run() 方法,即定义一个 run() 方法,并且在方法中实现要执行的代码。当我们调用自己编写的类创建出来的对象的 start() 方法时,会创建新的进程,并且进程会自动调用 run() 方法开始执行。
如果除了 run() 方法之外还定义了很多其它的方法,那么这些方法需要在 run() 方法中自己去第调用,进程它不会自动调用。
import time
from multiprocessing import Process
class MyProcess(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"{self.name}开始执行")
time.sleep(1)
print(f"{self.name}执行结束")
"""
Window操作系统下,创建系统一定要在main内创建
因为Window系统下创建进程类似于模块的导入的方式,会从上往下依次执行代码
Linux中则是直接将代码完成拷贝一份
"""
if __name__ == "__main__":
p1 = MyProcess("进程1")
p2 = MyProcess("进程2")
p1 .start()
p2.start()
print("主进程执行")
创建进程就是在内存中申请一块内存空间将需要运行的代码丢进去;一个进程对应在内存中就是一块独立的内存空间,多个进程对应在内存中就是多块独立的内存空间。默认情况下,进程与进程之间时无法直接交互的。如果想交互,可以借助第三方模块。
四、进程的常用属性和方法
multiprocessing.process.name # 当前进程实例别名,默认为Process-N,N从1开始递增的整数
multiprocessing.process.pid # 当前进程实例的PID值
multiprocessing.process.start() # 启动进程实例
multiprocessing.process.run() # 如果没有给定target参数,对这个对象调用start()方法时,就会执行对象中的run()方法
multiprocessing.process.is_alive() # 判断进程实例是否还在执行
multiprocessing.process.join(timeout=None) # 是否等待进程实际执行结束,或等得多少秒
multiprocessing.process.terminate() # 不管任务是否完成,立即终止
import time
from multiprocessing import Process, current_process
money = 100
def task(n):
print(f"{current_process().name }- {current_process().pid}")
print(f"{current_process().name}开始执行")
global money
money *= n
time.sleep(n)
print(f"{current_process().name}的money: {money}")
print(f"{current_process().name}执行结束")
"""
Window操作系统下,创建系统一定要在main内创建
因为Window系统下创建进程类似于模块的导入的方式,会从上往下依次执行代码
Linux中则是直接将代码完成拷贝一份
"""
if __name__ == "__main__":
## 1、实例化对象
p1 = Process(target=task, args=(1,))
p2 = Process(target=task, args=(2,))
p3 = Process(target=task, args=(3,))
start_time = time.time()
# 2、开启进程
p1.start() # 告诉操作系统帮你创建一个进程
p2.start()
p3.start()
p2.terminate() # 告诉操作系统,终止进程,但是需要一定的时间
print(p2.is_alive()) # 获取进程状态
# 主进程等待子进程运行结束之后在继续往后执行
p3.join()
print(f"{current_process().name} {time.time() - start_time}")
print(f"{current_process().name} money: {money}")
五、特殊的进程
5.1、僵尸进程与孤儿进程
当你开设子进程之后,该进程死后不会立即释放占用的进程号。这是因为要让父进程能够查看它开设的子进程的一些基本信息,例如:占用的 pid 号、运行时间等;这种的进程称为 僵尸进程。所有的进程都会步入僵尸进程。
孤儿进程 是指子进程存活,父进程意外死亡的进程。操作系统会开设一个特殊的空间专门管理孤儿进程回收相关资源。
5.2、守护进程
守护进程,专门用于服务其它的进程。当所有非守护进程结束时,没有了被守护者,守护进程也就没有工作可做,当然也就没有继续执行的必要了,程序就会终止,同时会杀死所有的 "守护进程",也就是说只要有任何非守护进程还在运行,程序就不会终止。被守护的进程结束之后,守护进程也会立即跟着结束。如果我们想把一个进程设置为守护进程,那么需要在调用 start() 方法前把 daemon 属性设置为 True。
import time
from multiprocessing import Process
def task(name, n):
print(f"{name}开始执行")
time.sleep(n)
print(f"{name}执行结束")
"""
Window操作系统下,创建系统一定要在main内创建
因为Window系统下创建进程类似于模块的导入的方式,会从上往下依次执行代码
Linux中则是直接将代码完成拷贝一份
"""
if __name__ == "__main__":
p1 = Process(target=task, args=("守护进程1", 3), daemon=True)
p2 = Process(target=task, args=("守护进程2", 3)) # 1、实例化对象
p2.daemon = True # 2、将进程设置为守护进程
p1.start() # 3、开启进程,告诉操作系统帮你创建一个进程
p2.start()
time.sleep(1)
print("主进程执行")
六、互斥锁
6.1、什么是互斥锁
多个进程操作同一份数据时,可能会出现数据错乱的问题。针对上述问题,解决方式就是 加锁处理:将并发变成串行,牺牲效率但保证了数据的安全。
在操作数据前,我们使用 Lock.acquire() 方法 获取锁,如果锁是空闲的,则会立即上锁,程序继续往下执行。如果锁被占用,则当前进程会阻塞在这里,直到获取到锁位置。操作完数据之后,我们需要使用 Lock.release() 方法 释放锁。对于互斥锁,我们也可以使用 with 关键字进行上下文管理。
import time
import json
from multiprocessing import Process, Lock
def buy(name, lock):
lock.acquire() # 获取锁
# 先查剩余的票数
with open("data.json", "r", encoding="utf-8") as f:
ticket_dict = json.load(f)
time.sleep(1) # 模拟网络延迟
# 判断当前是否有票
if ticket_dict.get("ticket_num") > 0:
ticket_dict["ticket_num"] -= 1 # 修改数据买票
# 写入数据
with open("data.json", "w", encoding="utf-8") as f:
json.dump(ticket_dict, f)
print(f"用户{name}买票成功")
else:
print(f"用户{name}买票失败")
lock.release() # 释放锁
"""
Window操作系统下,创建系统一定要在main内创建
因为Window系统下创建进程类似于模块的导入的方式,会从上往下依次执行代码
Linux中则是直接将代码完成拷贝一份
"""
if __name__ == "__main__":
# 在主进程中生成一把锁,让所有的进程程抢,谁先抢到谁先买票
lock = Lock()
p1 = Process(target=buy, args=("Sakura", lock)) # 1、实例化对象
p1.start() # 2、开启进程,告诉操作系统帮你创建一个进程
p2 = Process(target=buy, args=("Mikoto", lock))
p2.start()
p3 = Process(target=buy, args=("Shana", lock))
p3.start()
【data.json】文本内容如下:
{
"ticket_num": 2
}
锁应该只在处理数据的部分加锁保证数据安全。
推荐使用
with关键字进行上下文管理的方式使用锁,使用手动的方式,如果代码在释放锁之前发生异常,则会导致该进程卡死,一直不会释放锁,其它进程则会进入阻塞状态,一直尝试获取锁。而使用的with的方式,发生异常后,也能正常释放锁。
6.2、递归锁
如果在一个进程接连两次使用 Lock.acquire() 方法 获取锁,会因为第二次获取锁时,锁的状态被占用,从而阻塞进程,直到获取锁。但由于该进程一直无法释放锁,从而一直会一直卡住。如果我们想要在同一个进程中 重复上锁,需要使用 RLock 递归锁。
递归锁 可以被连续的获取和释放,但是只能被第一个抢到这把锁的进程执行上述操作。递归锁的内部有一个计数器,每获取锁,则计数加 1,每释放一次锁,则计数减 1,只要计数不为 0,那么其它人都无法获取到这个锁。
from multiprocessing import Process, RLock
class MyProcess(Process):
def __init__(self, name, lock):
super().__init__()
self.name = name
self.lock = lock
def run(self):
self.task()
def task(self):
with self.lock:
print(f"{self.name}在task函数中获取锁")
self.fun()
print(f"{self.name}在task函数中释放锁")
def fun(self):
with self.lock:
print(f"{self.name}在fun函数中获取锁")
print(f"{self.name}在fun函数中释放锁")
"""
Window操作系统下,创建系统一定要在main内创建
因为Window系统下创建进程类似于模块的导入的方式,会从上往下依次执行代码
Linux中则是直接将代码完成拷贝一份
"""
if __name__ == "__main__":
lock = RLock()
p1 = MyProcess("进程1", lock)
p1.start()
6.3、死锁问题
不同的进程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了进程间的 死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的进程都处于阻塞状态,无法继续。
import time
from multiprocessing import Process, Lock
def task1(name, lock1, lock2):
lock1.acquire()
print(f"{name}获取到锁1")
time.sleep(3)
lock2.acquire()
print(f"{name}获取到锁2")
lock1.release()
print(f"{name}释放锁1")
lock2.release()
print(f"{name}释放锁2")
def task2(name, lock1, lock2):
lock2.acquire()
print(f"{name}获取到锁2")
time.sleep(3)
lock1.acquire()
print(f"{name}获取到锁1")
lock2.release()
print(f"{name}释放锁2")
lock1.release()
print(f"{name}释放锁1")
"""
Window操作系统下,创建进程一定要在main内创建
因为Window系统下创建进程类似于模块的导入的方式,会从上往下依次执行代码
Linux中则是直接将代码完成拷贝一份
"""
if __name__ == "__main__":
lock1 = Lock()
lock2 = Lock()
p1 = Process(target=task1, args=("进程1", lock1, lock2))
p2 = Process(target=task2, args=("进程2", lock1, lock2))
p1.start()
p2.start()
七、进程通信
7.1、为什么需要进程通信
创建进程就是在内存中申请一块内存空间将需要运行的代码丢进去。一个进程对应在内存中就是一块独立的内存空间,多个进程对应在内存中就是多块独立的内存空间。默认情况下,进程与进程之间时无法直接交互的。如果想交互,可以借助第三方模块队列类实现。
当创建一个子进程的时候,会复制父进程的很多东西(全局变量等)。子进程和主进程是单独的两个进程,当一个进程结束的时候,不会对其它进程产生影响。
import time
from multiprocessing import Process
num = 100
def task1():
global num
num = 300
print(f"task1中的num:{num}")
def task2():
print(f"task2中的num:{num}")
if __name__ == "__main__":
p1 = Process(target=task1)
p2 = Process(target=task2)
# 先让p1线程执行
p1.start()
# 让主进程延迟1s,保证p1进程执行完之后,在执行p2进程
time.sleep(1)
# 让p2进程开始执行,看看获取的值是否是p1进程修改后的值
p2.start()
7.2、使用Queue实现进程通信
如果我们想让多个进程间共享数据,可以通过队列来实现。队列 (Queue)是具有一定约束的线性表,它只能在 一端插入 ( 入队 ,AddQ)而在 另一端删除 ( 出队 ,DeleteQ)。它具有 先进先出 (FIFO)的特性。,它的常用方法如下:
# 生成一个最大容量为maxsize队列,如果maxsize为0,则不指定队列大小
multiprocessing.Queue(maxsize=0, *, ctx)
multiprocessing.Queue.qsize() # 返回当前队列包含的消息数量
multiprocessing.Queue.empty() # 返回队列是否为空
multiprocessing.Queue.full() # 返回队列是否已满
# 向队列中存取数据,默认情况下,如果队列已满,还要放数据,程序会阻塞,直到有位置让出来,不会报错
multiprocessing.Queue.put(obj, block=True, timeout=None)
# 向队列中存取数据,如果队列已满,还要放数据,程序会抛出异常
multiprocessing.Queue.put_nowait(obj)
# 取队列中的数据,默认情况下,如果队列中没有数据,还要取数据,程序会阻塞,直到有新的数据到来,不会报错
multiprocessing.Queue.get(block=True, timeout=None)
# 取队列中的数据,如果队列中没有数据,还要取数据,程序会抛出异常
multiprocessing.Queue.get_nowait()
队列的基本使用方法如下:
from multiprocessing import Queue
names = ["Sakura", "Mikoto", "Shana", "Akame", "Kurome"]
q = Queue(3)
print("向队列中存储数据")
i = 0
while not q.full():
q.put(names[i])
i += 1
# 如果消息队列已满,如果还要向队列中存储数据,程序会阻塞或抛出异常
try:
# 如果没有设置timeout,向已满队列存储数据会阻塞,直到有位置让出来
# 如果设置timeout,则会等待timeout秒,如果在此期间还没有位置空出来,程序会抛出异常
q.put(names[i], timeout=3)
except Exception:
print("队列已满,现有消息数量:%s" % q.qsize())
try:
# 向已满队列存储数据会抛出异常
q.put_nowait(names[i + 1])
except Exception:
print("队列已满,现有消息数量:%s" % q.qsize())
print("从队列中读取数据")
while not q.empty():
data = q.get()
print(f"读取的数据为{data}")
# 如果消息队列已空,如果还要从队列中读取数据,程序会阻塞或抛出异常
try:
# 如果没有设置timeout,向已满队列存储数据会阻塞,直到有位置让出来
# 如果设置timeout,则会等待timeout秒,如果在此期间还没有位置空出来,程序会抛出异常
q.get(timeout=3)
except Exception:
print("队列已空,现有消息数量:%s" % q.qsize())
try:
# 向已满队列存储数据会抛出异常
q.get_nowait()
except Exception:
print("队列已空,现有消息数量:%s" % q.qsize())
我们可以使用队列进行进程间的通信。
import time
import random
from multiprocessing import Process, Queue
def productor(name, food, q):
for i in range(10):
time.sleep(random.randint(1,3)) # 模拟延迟
data = f"【{name}】生产了第 {i+1} 个【{food}】"
print(data)
q.put(data) # 往队列中存入数据
def consumer(name, q):
while True:
time.sleep(random.randint(1,3)) # 模拟延迟
food = q.get() # 从队列中取出数据
print(f"【{name}】吃了 {food}")
if __name__ == "__main__":
q = Queue(10) # 创建队列
# 创建进程
p1 = Process(target=productor,args=("星光", "包子", q))
p2 = Process(target=productor, args=("冰心", "寿司", q))
c1 = Process(target=consumer, args=("小樱", q), daemon=True)
c2 = Process(target=consumer, args=("小娜", q), daemon=True)
# 启动进程
p1.start()
p2.start()
c1.start()
c2.start()
p1.join()
p2.join()
while not q.empty(): # 判断队列是否为空
pass
7.3、使用Pipe实现进程通信
在 Python 中,可以使用 multiprocessing 模块的 Pipe 来实现两个进程之间的通信。默认情况下管道是双向的(双工),即两端都可以发送 send() 和接收数据 recv()。
import time
import random
from multiprocessing import Process, Pipe
def productor(name, food, connection):
for i in range(10):
time.sleep(random.randint(1,3)) # 模拟延迟
data = f"【{name}】生产了第 {i+1} 个【{food}】"
print(data)
connection.send(data) # 往管道中发送数据
def consumer(name, connection):
while True:
time.sleep(random.randint(1,3)) # 模拟延迟
food = connection.recv() # 从管道中接收数据
print(f"【{name}】吃了 {food}")
if __name__ == "__main__":
# 如果我们指定duplex=False,则connection1只能用来接收,connection2只能用来发送
connection1, connection2 = Pipe(duplex=False) # 创建管道
# 创建进程
p1 = Process(target=productor,args=("星光", "包子", connection1))
p2 = Process(target=productor, args=("冰心", "寿司", connection1))
c1 = Process(target=consumer, args=("小樱", connection2), daemon=True)
c2 = Process(target=consumer, args=("小娜", connection2), daemon=True)
# 启动进程
p1.start()
p2.start()
c1.start()
c2.start()
p1.join()
p2.join()
7.4、Event事件
一些进程需要等待另外一些进程运行完毕之后才能运行,类似于发射信号一样。这时,我们可以使用 Event 事件。
multiprocessing.Event.set() # 将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态
multiprocessing.Event.clear() # 将标志设为False
multiprocessing.Event.wait(timeout=None) # 如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()
multiprocessing.Event.is_set() # 获取内置标志状态,返回True或False
事件 Event 中有一个全局内置标志 flag。使用 wait() 函数的进程会处于 阻塞状态,此时 flag 值为 False,直到有其它进程调用 set() 函数让全局标志 flag 置为 True,其阻塞的进程立刻恢复运行,还可以用 is_set() 函数检查当前的 flag 状态。
import time
from multiprocessing import Process, Event
def light(event):
print("红灯亮着呢")
time.sleep(3)
print("绿灯亮了")
# 告诉等待红灯的人可以走了
event.set()
def car(name, event):
print(f"{name}正在等红灯")
# 别人通知不要等了
event.wait() # 等待别人给你发信号
print(f"{name}开走了")
if __name__ == "__main__":
event = Event()
p = Process(target=light, args=(event,))
p.start()
for i in range(20):
p = Process(target=car, args=(f"小车{i}", event))
p.start()
八、进程池
池是用来保证计算机硬件安全的情况下最大限度的利用计算机,它降低了程序的运行效率,但是保证了计算机硬件的安全,从而让你写的程序能够正常运行。
8.1、Pool进程池
在 Python 2.6 版本时,提供了 Pool 进程池。我们初始化 Pool 时,可以指定一个 最大进程数,当有新的请求提交到 Pool 时,如果池还没有满,那么就会创建一个新的进程用来执行该请求。但是如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务。全部的进程执行完毕之后,我们需要调用 close() 方法 关闭进程池。
import time
import os
import random
from multiprocessing import Pool
def task(name):
print(f"【{name}】开始执行,进程号为:{os.getpid()}")
time.sleep(random.randint(1,3))
print(f"【{name}】执行结束")
return f"这是【{name}】的执行结果"
if __name__ == "__main__":
# 括号内可以传数字指定进程数,不传的话,默认会开设当前计算机CPU个数的进程
# 池子造出来后,会存在一定数量的进程,这些进程不会出现重复创建和销毁的过程
pool = Pool()
# 池子的使用非常简单,只需要将需要做的任务往池子中提交即可
futures = [pool.apply_async(task, args=(f"任务-{i}",)) for i in range(20)]
# 等待进程池中所有的任务执行完毕之后再继续往下执行
pool.close() # 关闭进程池,等待进程中所有任务运行完毕
pool.join() # 主进程等待子进程全部执行完
for future in futures:
print(future.get()) # 拿到异步提交的返回结果
print("主线程执行完毕")
8.2、进程池执行器
在 Python 3.2 版本之后,提供了 ProcessPoolExecutor 进程池执行器 来使用进程池,在创建的过程中,我们可以通过 max_workers 参数指定 最大进程数。
创建进程执行器之后,我们需要调用 submit() 方法 提交任务。如果池还没有满,那么就会创建一个新的进程用来执行该请求。但是如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务。
当所有进程执行完毕之后,我们需要调用 shutdown() 方法 关闭进程池执行器。当然,我们可以使用 with 关键字进行上下文管理自动关闭进程池执行器。
import time
import os
import random
from concurrent.futures import ProcessPoolExecutor
def task(name):
print(f"【{name}】开始执行,进程号为:{os.getpid()}")
time.sleep(random.randint(1,3))
print(f"【{name}】执行结束")
return f"这是【{name}】的执行结果"
if __name__ == "__main__":
# 括号内可以传数字指定进程数,不传的话,默认会开设当前计算机CPU个数的进程
# 池子造出来后,会存在一定数量的进程,这些进程不会出现重复创建和销毁的过程
executor = ProcessPoolExecutor(5)
# 池子的使用非常简单,只需要将需要做的任务往池子中提交即可
futures = [executor.submit(task, f"任务-{i}") for i in range(20)]
# 等待进程池中所有的任务执行完毕之后再继续往下执行
executor.shutdown(wait=True) # 关闭进程池,等待进程中所有任务运行完毕
for future in futures:
print(future.result()) # 拿到异步提交的返回结果
print("主进程执行完毕")
默认情况下,我们获取任务的结果是按 提交任务顺序 得到结果的。如果我们想要按 完成任务顺序 得到结果,则可以使用 as_completed()。
import time
import os
import random
import concurrent.futures
from concurrent.futures import ProcessPoolExecutor
def task(name):
print(f"【{name}】开始执行,进程号为:{os.getpid()}")
time.sleep(random.randint(1,3))
print(f"【{name}】执行结束")
return f"这是【{name}】的执行结果"
if __name__ == "__main__":
results = []
# 括号内可以传数字指定进程数,不传的话,默认会开设当前计算机CPU个数的进程
# 池子造出来后,会存在一定数量的进程,这些进程不会出现重复创建和销毁的过程
with ProcessPoolExecutor(5) as executor:
# 池子的使用非常简单,只需要将需要做的任务往池子中提交即可
futures = [executor.submit(task, f"任务-{i}") for i in range(20)]
for future in concurrent.futures.as_completed(futures):
results.append(future.result()) # 拿到异步提交的返回结果
for result in results:
print(result)
print("主进程执行完毕")
我们还可以使用 add_done_callback() 方法为 任务添加完成时的回调函数。
import time
import os
import random
from concurrent.futures import ProcessPoolExecutor
def task(name):
print(f"【{name}】开始执行,进程号为:{os.getpid()}")
time.sleep(random.randint(1,3))
print(f"【{name}】执行结束")
return f"这是【{name}】的执行结果"
def done_func(future):
print(future.result())
if __name__ == "__main__":
results = []
# 括号内可以传数字指定进程数,不传的话,默认会开设当前计算机CPU个数的进程
# 池子造出来后,会存在一定数量的进程,这些进程不会出现重复创建和销毁的过程
with ProcessPoolExecutor(5) as executor:
# 池子的使用非常简单,只需要将需要做的任务往池子中提交即可
futures = [executor.submit(task, f"任务-{i}") for i in range(20)]
for future in futures:
future.add_done_callback(done_func) # 添加回调函数
print("主进程执行完毕")
如果我们要批量提交任务,则可以使用 map() 方法。map() 方法是 阻塞的,并且它得到 结果的顺序 与 任务分配的顺序 一致。
import time
import os
import random
from concurrent.futures import ProcessPoolExecutor
def task(name):
print(f"【{name}】开始执行,进程号为:{os.getpid()}")
time.sleep(random.randint(1,3))
print(f"【{name}】执行结束")
return f"这是【{name}】的执行结果"
if __name__ == "__main__":
results = []
# 括号内可以传数字指定进程数,不传的话,默认会开设当前计算机CPU个数的进程
# 池子造出来后,会存在一定数量的进程,这些进程不会出现重复创建和销毁的过程
with ProcessPoolExecutor(5) as executor:
# 池子的使用非常简单,只需要将需要做的任务往池子中提交即可
results = executor.map(task, [f"任务-{i}" for i in range(20)])
for result in results:
print(result)
print("主进程执行完毕")
8.3、进程池间通信
进程池间通信要使用 Manage 创建的 Queue 队列,不能直接使用普通的 Queue。
import time
import os
from multiprocessing import Manager
from concurrent.futures import ProcessPoolExecutor
def reader(q):
print(f"reader启动({os.getpid()}),父进程({os.getppid()})")
for _ in range(q.qsize()):
print(f"reader从Queue获取消息:{q.get()}")
def write(q, datas):
print(f"write启动({os.getpid()}),父进程({os.getppid()})")
for data in datas:
q.put(data)
if __name__ == "__main__":
q = Manager().Queue()
# 括号内可以传数字指定进程数,不传的话,默认会开设当前计算机CPU个数的进程
# 池子造出来后,会存在一定数量的进程,这些进程不会出现重复创建和销毁的过程
with ProcessPoolExecutor(5) as executor:
executor.submit(write, q, ["Sakura", "Mikoto", "Shana"]) # 朝池子中提交任务,异步提交
# 先让上面的任务向Queue存入数据,然后在让下面的任务从中读取数据
time.sleep(1)
executor.submit(reader, q) # 朝池子中提交任务,异步提交

浙公网安备 33010602011771号