文章目录
- [一、创建进程 Process 对象](#一、创建进程 Process 对象)
-
- [1、实例化:multiprocessing.Process(target=目标函数, args=(函数位置参数,), kwargs={函数关键字参数})](#1、实例化:multiprocessing.Process(target=目标函数, args=(函数位置参数,), kwargs={函数关键字参数}))
- 2、开始工作:multiprocessing.Process(~).start()
- [3、多进程对 \\main\\ 的额外保护机制](#3、多进程对 __main__ 的额外保护机制)
-
- [3.1 没有__main__检查语句可能触发的异常](#3.1 没有__main__检查语句可能触发的异常)
- [3.2 解决方案1:添加__main__检查语句](#3.2 解决方案1:添加__main__检查语句)
- [3.3 解决方案2:从独立脚本中导入目标函数](#3.3 解决方案2:从独立脚本中导入目标函数)
- 二、当前进程和主进程
-
- [1、自定义进程名称:multiprocessing.Process(name='xxx') / .name='xxx'](#1、自定义进程名称:multiprocessing.Process(name='xxx') / .name='xxx')
- 2、获取当前进程名称:multiprocessing.current_process().name
- 3、获取当前进程的唯一id:multiprocessing.current_process().pid
- [4、获取 CPU 核心数:multiprocessing.cpu_count()](#4、获取 CPU 核心数:multiprocessing.cpu_count())
- 三、守护与非守护进程
-
- [1、创建守护进程:multiprocessing.Process(daemon=True) / .daemon = True](#1、创建守护进程:multiprocessing.Process(daemon=True) / .daemon = True)
- 2、等待守护进程完成:进程对象.join(可选的秒数)
- 3、判断进程是否处于活跃状态:进程对象.is_alive()
- 四、进程终止与退出状态码
-
- [1、强制终止进程:进程对象.terminate() + .join()](#1、强制终止进程:进程对象.terminate() + .join())
- 2、进程退出状态码:进程对象.exitcode
- 五、日志
- 六、进程继承
-
- [1、继承 multiprocessing.Process 类,重写 run() 方法](#1、继承 multiprocessing.Process 类,重写 run() 方法)
- 七、进程间通信
-
- 1、FIFO队列:multiprocessing.Queue()
-
- [1.1 .get():阻塞等待,直至拿到数据](#1.1 .get():阻塞等待,直至拿到数据)
- [1.2 .put():向队列中放入数据,触发「阻塞等待的.get()」执行](#1.2 .put():向队列中放入数据,触发「阻塞等待的.get()」执行)
- [1.3 .close():关闭队列,禁止再放入数据](#1.3 .close():关闭队列,禁止再放入数据)
- [1.4 .join_thread():等待队列的后台线程完成数据传输](#1.4 .join_thread():等待队列的后台线程完成数据传输)
- 2、支持任务追踪的队列:multiprocessing.JoinableQueue()
-
- [2.1 .get():阻塞等待,直至拿到数据](#2.1 .get():阻塞等待,直至拿到数据)
- [2.2 .put():向队列中放入数据,触发「阻塞等待的.get()」执行](#2.2 .put():向队列中放入数据,触发「阻塞等待的.get()」执行)
- [2.3 .task_done():标记当前任务完成,告知队列「该任务已处理」](#2.3 .task_done():标记当前任务完成,告知队列「该任务已处理」)
- [2.4 .join():阻塞直到队列中的所有任务都被 task_done() 标记完成](#2.4 .join():阻塞直到队列中的所有任务都被 task_done() 标记完成)
- 3、事件对象:multiprocessing.Event()
-
- [3.1 .set() / .clear() / .is_set():事件「已设置 / 未设置 / 是否被设置」](#3.1 .set() / .clear() / .is_set():事件「已设置 / 未设置 / 是否被设置」)
- [3.2 .wait(可选的秒数):暂停运行,直到内部标志被设置 / 等待超时](#3.2 .wait(可选的秒数):暂停运行,直到内部标志被设置 / 等待超时)
- 八、控制资源访问
-
- 1、建立互斥锁:multiprocessing.Lock()
- [2、获取锁与释放锁:multiprocessing.Lock().acquire(是否阻塞, 阻塞多久) / .release()](#2、获取锁与释放锁:multiprocessing.Lock().acquire(是否阻塞, 阻塞多久) / .release())
- 3、可重入锁:multiprocessing.RLock(),允许同一进程多次获取同一把锁
- [4、锁作为上下文管理器:「with 锁对象:」](#4、锁作为上下文管理器:「with 锁对象:」)
- 九、同步进程
-
- 1、条件对象:multiprocessing.Condition()
-
- [1.1 条件对象.wait():让进程进入等待状态,临时释放内部锁,直到被其他进程唤醒](#1.1 条件对象.wait():让进程进入等待状态,临时释放内部锁,直到被其他进程唤醒)
- [1.2 条件对象.notify() / .notify_all():唤醒一个或所有等待该条件的进程](#1.2 条件对象.notify() / .notify_all():唤醒一个或所有等待该条件的进程)
- [1.3 with 条件对象:通过上下文管理器自动获取 / 释放内部锁](#1.3 with 条件对象:通过上下文管理器自动获取 / 释放内部锁)
- [1.4 (不推荐)显示使用 acquire() 和 release() 方法](#1.4 (不推荐)显示使用 acquire() 和 release() 方法)
- 十、限制资源的并发访问
-
- [1、with multiprocessing.Semaphore(最大并发数量)](#1、with multiprocessing.Semaphore(最大并发数量))
- 2、基于管理器创建共享资源:multiprocessing.Manager()
-
- [2.1 共享列表:multiprocessing.Manager().list()](#2.1 共享列表:multiprocessing.Manager().list())
- [2.2 共享字典:multiprocessing.Manager().dict()](#2.2 共享字典:multiprocessing.Manager().dict())
- [2.3 共享命名空间:multiprocessing.Manager().Namespace()](#2.3 共享命名空间:multiprocessing.Manager().Namespace())
- 十一、进程池:管理固定数目的工作进程
-
- [1、创建进程池:multiprocessing.Pool(processes, initializer, maxtasksperchild)](#1、创建进程池:multiprocessing.Pool(processes, initializer, maxtasksperchild))
- [2、进程池对象.map(func, iterable, chunksize)](#2、进程池对象.map(func, iterable, chunksize))
- 3、进程池对象.close():关闭进程池,禁止提交新任务
- 4、进程池对象.join():等待所有子进程完成任务
- [十二、实现单服务器 MapReduce](#十二、实现单服务器 MapReduce)
multiprocessing模块提供了一套 API,可基于threading模块的 API 将任务拆分到多个进程中执行。在某些场景下,multiprocessing可以直接替换threading来使用:借助多个 CPU 核心的优势,规避因 Python 全局解释器锁(GIL)导致的计算性能瓶颈。
python
import multiprocessing
一、创建进程 Process 对象
1、实例化:multiprocessing.Process(target=目标函数, args=(函数位置参数,), kwargs={函数关键字参数})
向multiprocessing模块的Process类传递:目标函数到target参数、目标函数所需位置参数到args、目标函数所需关键字参数到kwargs,就可以实例化一个Process对象。
注意,与threading不同的是,向multiprocessing.Process对象传递参数时,这些参数必须能够通过pickle进行序列化。具体原因如下:
- 多线程
threading:线程共享进程内存,参数可以直接传递(无需序列化); - 多进程
multiprocessing:每个进程有独立内存空间,参数需要先通过pickle序列化为字节流,传递到子进程后再反序列化,因此只有可被pickle序列化的对象才能作为参数。
2、开始工作:multiprocessing.Process(~).start()
调用start()方法就可以让实例化对象开始工作。
python
def worker(num):
print('Worker:', num)
if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=worker, args=(i,))
p.start()
3、多进程对 main 的额外保护机制
multiprocessing对__main__增加了额外的保护机制。由于新进程的启动方式特殊,子进程需要能够导入包含目标函数的脚本文件。将程序的主逻辑包裹在一个__main__检查语句中,可确保模块被导入时,主逻辑不会在每个子进程中递归执行。另一种解决方案是,从独立的脚本文件中导入目标函数。
3.1 没有__main__检查语句可能触发的异常
对于 Linux / macOS 系统,子进程会直接复制父进程内存,无需重新导入包含目标函数的脚本文件,因此__main__检查可选。
而对于 Windows 系统,子进程会重新启动 Python 解释器,再次完整导入包含目标函数的脚本文件,因此__main__检查是必选项。
python
# bad_example.py(Windows下运行会崩溃)
import multiprocessing
def worker():
print('Worker')
# 无__main__检查,直接创建进程
for _ in range(5):
p = multiprocessing.Process(target=worker)
p.start()
- 对于上述代码,在执行
python bad_example.py时,主进程会创建子进程; - Windows 子进程启动时,会重新导入
bad_example.py,此时代码会再次执行循环创建进程,引发「无限递归创建进程」,最终导致系统崩溃(报错RuntimeError或进程数爆炸)。
3.2 解决方案1:添加__main__检查语句
添加if __name__ == '__main__':后,脚本的执行逻辑变为:
- 主进程执行时:
__name__是'__main__',会执行循环创建进程; - 子进程导入脚本时:
__name__是'bad_example'(脚本名),不会执行if内的主逻辑,仅导入worker函数,避免递归创建进程。
python
# good_example.py(Windows/Linux通用)
import multiprocessing
def worker():
print('Worker')
# 主逻辑包裹在__main__检查中
if __name__ == '__main__':
for _ in range(5):
p = multiprocessing.Process(target=worker)
p.start()
3.3 解决方案2:从独立脚本中导入目标函数
将目标函数放到单独的文件中,彻底规避导入时的递归问题:
- 核心优势:子进程导入
worker_module.py时,仅获取worker函数,无任何主逻辑,从根源避免递归执行; - 适用场景:大型项目(多进程逻辑复杂,拆分模块更易维护)。
python
# worker_module.py
def worker():
print('Worker')
python
# main.py(无需__main__检查也能运行,推荐大型项目使用)
import multiprocessing
from worker_module import worker # 从独立模块导入函数
for _ in range(5):
p = multiprocessing.Process(target=worker)
p.start()
二、当前进程和主进程
1、自定义进程名称:multiprocessing.Process(name='xxx') / .name='xxx'
每个Process实例都有一个默认名称,主进程默认名称是'MainProcess',子进程默认是'Process-n',在创建进程对象时配置name参数可以改变该默认名称。
2、获取当前进程名称:multiprocessing.current_process().name
注意,Python 3.10+ 版本移除了multiprocessing.Process的setName() / getName()方法。
python
import time
def worker():
name = multiprocessing.current_process().name
print(name, 'Starting')
time.sleep(2)
print(name, 'Exiting')
def my_service():
name = multiprocessing.current_process().name
print(name, 'Starting')
time.sleep(3)
print(name, 'Exiting')
if __name__ == '__main__':
service = multiprocessing.Process(
name='my_service',
target=my_service,
)
worker_1 = multiprocessing.Process(
target=worker,
)
worker_1.name = 'worker 1'
worker_2 = multiprocessing.Process( # default name
target=worker,
)
worker_1.start()
worker_2.start()
service.start()
注意,获取程序主进程名称没有额外的方法,在执行if __name__ == '__main__':代码块的进程中直接调用current_process().name直接获取即可。
3、获取当前进程的唯一id:multiprocessing.current_process().pid
4、获取 CPU 核心数:multiprocessing.cpu_count()
三、守护与非守护进程
默认情况下,主程序会等待所有子进程执行完毕后才退出。但在某些场景下,启动一个「守护进程」会更实用:这类进程在运行时不会阻塞主程序退出。
使用守护进程适用于以下场景:要么难以中断该进程,要么允许进程在工作中途终止且不会导致数据丢失或损坏。线程的默认状态为非守护进程。
1、创建守护进程:multiprocessing.Process(daemon=True) / .daemon = True
注意:
- Python 3.10+ 版本移除了
multiprocessing.Process的setDaemon(True)方法。 - 守护进程会在主程序退出前被自动终止,从而避免出现孤立进程持续运行的情况。(验证进程终止:执行
ps -ef | grep pid号(Linux)或 ps aux | grep pid号(macOS)查看进程状态,如果输出为空 / 无pid号进程,则说明守护进程已被终止。)
下列程序的运行结果中并未包含守护进程的「Exiting信息」,这是因为在守护进程从其2秒的休眠中唤醒之前,所有非守护进程(包括主程序本身)就已经退出:
python
import time
import sys
def daemon():
p = multiprocessing.current_process()
print('Starting:', p.name, p.pid)
sys.stdout.flush() # 强制刷新输出缓冲区,确保立即打印(避免多进程输出错乱,但仅在「大量高频打印」或「进程快速退出」场景下有必要)
time.sleep(2)
print('Exiting :', p.name, p.pid)
sys.stdout.flush()
def non_daemon():
p = multiprocessing.current_process()
print('Starting:', p.name, p.pid)
sys.stdout.flush()
print('Exiting :', p.name, p.pid)
sys.stdout.flush()
if __name__ == '__main__':
d = multiprocessing.Process(
name='daemon',
target=daemon,
)
d.daemon = True
# 默认就是非守护进程
n = multiprocessing.Process(
name='non-daemon',
target=non_daemon,
)
d.start()
time.sleep(1)
n.start()
>>> 输出结果:
Starting: daemon 81860
Starting: non-daemon 81865
Exiting : non-daemon 81865
2、等待守护进程完成:进程对象.join(可选的秒数)
3、判断进程是否处于活跃状态:进程对象.is_alive()
使用join()方法可以等待一个守护进程完成工作,非守护进程也可以添加join()方法。
python
import time
def daemon():
name = multiprocessing.current_process().name
print('Starting:', name)
time.sleep(2)
print('Exiting :', name)
def non_daemon():
name = multiprocessing.current_process().name
print('Starting:', name)
print('Exiting :', name)
if __name__ == '__main__':
d = multiprocessing.Process(
name='daemon',
target=daemon,
daemon=True
)
n = multiprocessing.Process(
name='non-daemon',
target=non_daemon,
)
d.start()
time.sleep(1)
n.start()
d.join()
n.join()
>>> 输出结果:
Starting: daemon
Starting: non-daemon
Exiting : non-daemon
Exiting : daemon
默认情况下,join()方法会无限期阻塞。当然,也可以向该方法传递一个浮点值,该值表示等待进程结束(变为非活跃状态)的最长时间(秒)。即使进程在达到最长时间后仍然没有结束,join()方法也会停止阻塞。
python
import time
def daemon():
name = multiprocessing.current_process().name
print('Starting:', name)
time.sleep(2)
print('Exiting :', name)
def non_daemon():
name = multiprocessing.current_process().name
print('Starting:', name)
print('Exiting :', name)
if __name__ == '__main__':
d = multiprocessing.Process(
name='daemon',
target=daemon,
daemon=True
)
n = multiprocessing.Process(
name='non-daemon',
target=non_daemon,
)
d.start()
n.start()
d.join(1)
print('d.is_alive()', d.is_alive())
n.join()
>>> 输出结果:
Starting: daemon
Starting: non-daemon
Exiting : non-daemon
d.is_alive() True
四、进程终止与退出状态码
1、强制终止进程:进程对象.terminate() + .join()
终止进程后要使用join()方法等待进程退出,这能让进程管理代码有足够的时间来更新进程对象的状态,以反映进程已经终止。
python
import time
def slow_worker():
print('Starting worker')
time.sleep(0.1)
print('Finished worker')
if __name__ == '__main__':
p = multiprocessing.Process(target=slow_worker)
print('BEFORE:', p.name, p.is_alive())
p.start()
print('DURING:', p.name, p.is_alive())
p.terminate()
print('TERMINATED:', p.name, p.is_alive())
p.join()
print('JOINED:', p.name, p.is_alive())
>>> 输出结果:
BEFORE: Process-1 False
DURING: Process-1 True
TERMINATED: Process-1 True
JOINED: Process-1 False
2、进程退出状态码:进程对象.exitcode
进程退出时产生的状态码可通过exitcode属性获取,该属性允许的取值范围如下表所示:
| 退出码 | 含义 |
|---|---|
| == 0 | 未生成任何错误 |
| > 0 | 进程有一个错误,并以该错误码退出 |
| < 0 | 进程以一个- 1 * exitcode信号结束 |
python
import sys
import time
def exit_error():
# 主动退出进程,退出码设为1(非0表示异常退出)
sys.exit(1)
def exit_ok():
# 函数正常结束,进程无异常退出
return
def return_value():
# 进程仅执行函数返回,但退出码由进程退出状态决定,与返回值无关
return 1
def raises():
# 抛出未捕获异常,进程崩溃,产生异常的进程得到的 exitcode 为 1
raise RuntimeError('There was an error!')
def terminated():
# 休眠3秒,为后续强制终止提供时间窗口
time.sleep(3)
if __name__ == '__main__':
jobs = []
for f in [exit_error, exit_ok, return_value, raises, terminated]:
print('Starting process for', f.__name__)
j = multiprocessing.Process(target=f, name=f.__name__)
jobs.append(j)
j.start()
jobs[-1].terminate()
for j in jobs:
j.join()
print('{:>15}.exitcode = {}'.format(j.name, j.exitcode))
>>> 输出结果:
Starting process for exit_error
Starting process for exit_ok
Starting process for return_value
Starting process for raises
Starting process for terminated
Process raises:
exit_error.exitcode = 1
exit_ok.exitcode = 0
return_value.exitcode = 0
Traceback (most recent call last):
File ".../lib/python3.5/multiprocessing/process.py", line 313, in _bootstrap
self.run()
~~~~~~~~^^
File ".../lib/python3.5/multiprocessing/process.py", line 108, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/test.py", line 16, in raises
raise RuntimeError('There was an error!')
RuntimeError: There was an error!
raises.exitcode = 1
terminated.exitcode = -15 # Linux / macOS为-15,Windows为-1
五、日志
1、启用日志功能:multiprocessing.log_to_stderr(日志级别)
multiprocessing模块提供了一个模块级函数log_to_stderr()来启用日志功能。该函数使用logging模块创建一个日志器(logger)对象,并添加一个处理器(handler),使日志消息输出到标准错误(stderr)通道。
默认情况下,日志级别被设置为NOTSET,即不会生成任何日志消息。通过传入一个不同的日志级别参数,可以初始化日志器并指定所需的详细程度。
python
import logging
import sys
def worker():
print('Doing some work')
sys.stdout.flush()
if __name__ == '__main__':
multiprocessing.log_to_stderr(logging.DEBUG)
p = multiprocessing.Process(target=worker)
p.start()
p.join()
>>> 输出结果:
[INFO/Process-1] child process calling self.run()
Doing some work
[INFO/Process-1] process exiting with exitcode 0
[INFO/Process-1] process shutting down
[DEBUG/Process-1] running all "atexit" finalizers with priority >= 0
[DEBUG/Process-1] running the remaining "atexit" finalizers
[INFO/MainProcess] process shutting down
[DEBUG/MainProcess] running all "atexit" finalizers with priority >= 0
[DEBUG/MainProcess] running the remaining "atexit" finalizers
2、直接处理日志器:multiprocessing.get_logger().setLevel(日志级别)
使用get_logger()函数可以直接处理日志器(修改日志级别 or 增加处理器)。
python
import logging
import sys
def worker():
print('Doing some work')
sys.stdout.flush()
if __name__ == '__main__':
# 需要先启用日志功能
multiprocessing.log_to_stderr()
logger = multiprocessing.get_logger()
logger.setLevel(logging.INFO)
p = multiprocessing.Process(target=worker)
p.start()
p.join()
>>> 输出结果:
[INFO/Process-1] child process calling self.run()
Doing some work
[INFO/Process-1] process exiting with exitcode 0
[INFO/Process-1] process shutting down
[INFO/MainProcess] process shutting down
3、通过配置文件配置日志器:logging.config.fileConfig(日志配置文件)
还可以借助日志配置文件 API,通过指定名称multiprocessing来配置日志器。
-
step1:创建日志配置文件
logging.conf,文件中的#注释需要去掉[loggers]
keys=root,multiprocessing # 声明要配置的日志器,包含multiprocessing[logger_root]
level=WARNING
handlers=console[logger_multiprocessing] # 配置multiprocessing专属日志器
level=INFO # 日志级别
handlers=console,file # 输出到控制台+文件
qualname=multiprocessing # 关键:名称必须为"multiprocessing"
propagate=0 # 不向父日志器传播[handlers]
keys=console,file[handler_console]
class=StreamHandler
level=DEBUG
formatter=simple
args=(sys.stderr,)[handler_file]
class=FileHandler
level=DEBUG
formatter=detailed
args=('mp_config.log', 'a') # 追加模式写入文件[formatters]
keys=simple,detailed[formatter_simple]
format=%(levelname)s - %(message)s[formatter_detailed]
format=%(asctime)s - %(name)s - %(process)d - %(levelname)s - %(message)s
step2:在代码中加载配置文件
python
import multiprocessing
import logging
import logging.config
def worker():
print("Doing work")
if __name__ == '__main__':
# 需要先启用日志功能
multiprocessing.log_to_stderr()
# 加载日志配置文件
logging.config.fileConfig('logging.conf')
p = multiprocessing.Process(target=worker)
p.start()
p.join()
>>> 输出结果:
Doing work
[INFO/Process-1] process exiting with exitcode 0
[INFO/Process-1] process shutting down
INFO - process shutting down
六、进程继承
1、继承 multiprocessing.Process 类,重写 run() 方法
如果要创建multiprocessing.Process类的子类,只需重写run()方法来实现所需功能即可。注意,run()方法的返回值将被忽略。
python
class Worker(multiprocessing.Process):
def run(self):
print('In {}'.format(self.name))
return
if __name__ == '__main__':
for _ in range(5):
p = Worker()
p.start()
>>> 输出结果:(注意进程的名字取决于)
In Worker-3
In Worker-2
In Worker-1
In Worker-5
In Worker-4
七、进程间通信
1、FIFO队列:multiprocessing.Queue()
1.1 .get():阻塞等待,直至拿到数据
1.2 .put():向队列中放入数据,触发「阻塞等待的.get()」执行
1.3 .close():关闭队列,禁止再放入数据
1.4 .join_thread():等待队列的后台线程完成数据传输
能够用pickle串行化的任何对象都可以通过Queue传递。
python
class MyFancyClass:
def __init__(self, name):
self.name = name
def do_something(self):
proc_name = multiprocessing.current_process().name
print('Doing something fancy in {} for {}!'.format(proc_name, self.name))
def worker(q):
obj = q.get()
obj.do_something()
if __name__ == '__main__':
queue = multiprocessing.Queue()
p = multiprocessing.Process(target=worker, args=(queue,))
p.start()
queue.put(MyFancyClass('Fancy Dan'))
queue.close()
queue.join_thread()
p.join()
2、支持任务追踪的队列:multiprocessing.JoinableQueue()
2.1 .get():阻塞等待,直至拿到数据
2.2 .put():向队列中放入数据,触发「阻塞等待的.get()」执行
2.3 .task_done():标记当前任务完成,告知队列「该任务已处理」
每处理完一个任务必须调用该方法,用于JoinableQueue追踪「未完成任务数」。
2.4 .join():阻塞直到队列中的所有任务都被 task_done() 标记完成
python
import time
class Consumer(multiprocessing.Process):
def __init__(self, task_queue, result_queue):
multiprocessing.Process.__init__(self)
self.task_queue = task_queue
self.result_queue = result_queue
def run(self):
proc_name = self.name
while True:
next_task = self.task_queue.get()
if next_task is None:
print('{}: Exiting'.format(proc_name))
self.task_queue.task_done()
break
# 打印当前进程正在执行的任务(调用Task.__str__)
print('{}: {}'.format(proc_name, next_task))
# 调用 Task.__call__
answer = next_task()
# 无论从队列中取出的是什么数据,处理完当前数据都必须调用 .task_done() 方法
self.task_queue.task_done()
self.result_queue.put(answer)
class Task:
def __init__(self, a, b):
self.a = a
self.b = b
def __call__(self):
time.sleep(0.1)
return '{self.a} * {self.b} = {product}'.format(self=self, product=self.a * self.b)
def __str__(self):
return '{self.a} * {self.b}'.format(self=self)
if __name__ == '__main__':
# 创建通信队列
tasks = multiprocessing.JoinableQueue()
results = multiprocessing.Queue()
# 创建并启动消费者进程:数量为CPU核心数*2(此数字可以充分利用多核)
num_consumers = multiprocessing.cpu_count() * 2
print('Creating {} consumers'.format(num_consumers))
consumers = [Consumer(tasks, results) for _ in range(num_consumers)]
for w in consumers:
w.start()
# 生产者:放入10个计算任务到任务队列
num_jobs = 10
for i in range(num_jobs):
tasks.put(Task(i, i))
# 给每个消费者发一个 None,确保全部退出
for i in range(num_consumers):
tasks.put(None)
tasks.join()
while num_jobs:
result = results.get()
print('Result:', result)
num_jobs -= 1
>>> 输出结果:
Creating 8 consumers
Consumer-1: 0 * 0
Consumer-4: 1 * 1
Consumer-2: 2 * 2
Consumer-3: 3 * 3
Consumer-5: 4 * 4
Consumer-8: 5 * 5
Consumer-7: 6 * 6
Consumer-6: 7 * 7
Consumer-5: 8 * 8
Consumer-1: 9 * 9
Consumer-3: Exiting
Consumer-2: Exiting
Consumer-4: Exiting
Consumer-8: Exiting
Consumer-7: Exiting
Consumer-6: Exiting
Consumer-1: Exiting
Consumer-5: Exiting
Result: 0 * 0 = 0
Result: 3 * 3 = 9
Result: 2 * 2 = 4
Result: 4 * 4 = 16
Result: 1 * 1 = 1
Result: 5 * 5 = 25
Result: 6 * 6 = 36
Result: 7 * 7 = 49
Result: 8 * 8 = 64
Result: 9 * 9 = 81
3、事件对象:multiprocessing.Event()
3.1 .set() / .clear() / .is_set():事件「已设置 / 未设置 / 是否被设置」
每个Event对象各自管理着一个内部标志,可以通过set()和clear()方法来控制这个标志:
set()方法会将标志设为「已设置」状态clear()方法会将标志重置为「未设置」状态
可以用is_set()方法检查事件状态,判断事件是否已经被设置。调用该方法时不会阻塞进程。
3.2 .wait(可选的秒数):暂停运行,直到内部标志被设置 / 等待超时
其他进程可以使用wait()方法暂停运行,直到内部标志被设置,这实际上是在阻塞进程的执行,直到这些进程被允许继续执行。
wait()方法可以接受一个参数,表示等待事件的最长时间(秒),达到这个时间后就会超时并返回一个布尔值:
- 如果在超时时间内事件被设置,返回
True - 如果超时时间结束仍未被设置,返回
False
python
import time
def wait_for_event(e):
print('wait_for_event: starting')
e.wait()
print('wait_for_event: e.is_set()->', e.is_set())
def wait_for_event_timeout(e, t):
print('wait_for_event_timeout: starting')
e.wait(t)
print('wait_for_event_timeout: e.is_set()->', e.is_set())
if __name__ == '__main__':
e = multiprocessing.Event()
w1 = multiprocessing.Process(
name='block',
target=wait_for_event,
args=(e,),
)
w1.start()
w2 = multiprocessing.Process(
name='nonblock',
target=wait_for_event_timeout,
args=(e, 4),
)
w2.start()
print('main: waiting before calling Event.set()')
time.sleep(3)
e.set()
print('main: event is set')
>>> 输出结果:
main: waiting before calling Event.set()
wait_for_event_timeout: starting
wait_for_event: starting
main: event is set
wait_for_event: e.is_set()-> True
wait_for_event_timeout: e.is_set()-> True
八、控制资源访问
1、建立互斥锁:multiprocessing.Lock()
使用multiprocessing.Lock()来建立互斥锁,能够控制对共享资源的访问,从而避免破坏或者丢失数据,保证能够同时安全地访问一个对象。
2、获取锁与释放锁:multiprocessing.Lock().acquire(是否阻塞, 阻塞多久) / .release()
对互斥锁对象使用acquire()方法会获取锁,若锁被占用则会阻塞等待;使用release()方法则会释放锁。
acquire()方法中包含blocking和timeout两个可选参数,用于控制获取锁的行为(是否阻塞、阻塞多久),默认无限期阻塞 。注意,只有在参数blocking设置为True(默认值)时,参数timeout才会有效。
因此,如果想要知道其他进程是否已经获取了锁,但同时又不阻塞当前进程,可以给acquire()方法的blocking参数传入False。
3、可重入锁:multiprocessing.RLock(),允许同一进程多次获取同一把锁
在同一个进程内,正常的multiprocessing.Lock()对象不能在未释放锁之前连续请求多次,因为这会造成死锁。(除非设置blocking参数为False,或者设置timeout参数为非负数值)
multiprocessing.RLock()对象允许在同一进程中未释放锁之前连续请求多次,仅需保证release()次数与acquire()次数一致即可,不会造成死锁,这种锁叫可重入锁(Reentrant Lock)。
4、锁作为上下文管理器:「with 锁对象:」
锁实现了上下文管理器 API,并且与with语句兼容。使用with语句时不需要显式地获得锁与释放锁。
python
import sys
def worker_with(lock):
with lock:
sys.stdout.write('Lock acquired via with\n')
sys.stdout.flush()
def worker_no_with(lock):
lock.acquire()
try:
sys.stdout.write('Lock acquired directly\n')
sys.stdout.flush()
finally:
lock.release()
if __name__ == '__main__':
lock = multiprocessing.Lock()
w = multiprocessing.Process(
target=worker_with,
args=(lock,),
)
nw = multiprocessing.Process(
target=worker_no_with,
args=(lock,),
)
w.start()
nw.start()
w.join()
nw.join()
>>> 输出结果:
Lock acquired via with
Lock acquired directly
九、同步进程
1、条件对象:multiprocessing.Condition()
1.1 条件对象.wait():让进程进入等待状态,临时释放内部锁,直到被其他进程唤醒
1.2 条件对象.notify() / .notify_all():唤醒一个或所有等待该条件的进程
1.3 with 条件对象:通过上下文管理器自动获取 / 释放内部锁
1.4 (不推荐)显示使用 acquire() 和 release() 方法
进程可以使用with语句来获取与条件对象关联的锁,也可以显式地使用acquire()和release()方法。
python
import time
def stage_1(cond):
name = multiprocessing.current_process().name
print('Starting', name)
with cond:
print('{} done and ready for stage 2'.format(name))
cond.notify_all()
def stage_2(cond):
name = multiprocessing.current_process().name
print('Starting', name)
with cond:
cond.wait()
print('{} running'.format(name))
if __name__ == '__main__':
condition = multiprocessing.Condition()
s1 = multiprocessing.Process(name='s1',
target=stage_1,
args=(condition,))
s2_clients = [
multiprocessing.Process(
name='stage_2[{}]'.format(i),
target=stage_2,
args=(condition,),
)
for i in range(1, 3)
]
for c in s2_clients:
c.start()
time.sleep(1)
s1.start()
s1.join()
for c in s2_clients:
c.join()
>>> 输出结果:
Starting stage_2[1]
Starting stage_2[2]
Starting s1
s1 done and ready for stage 2
stage_2[2] running
stage_2[1] running
十、限制资源的并发访问
1、with multiprocessing.Semaphore(最大并发数量)
有时可能需要允许多个进程同时访问一个资源,但是要限制总数,使用multiprocessing.Semaphore(最大并发数量)就可以实现这个需求。
2、基于管理器创建共享资源:multiprocessing.Manager()
Manager负责协调所有进程之间共享的信息状态。
2.1 共享列表:multiprocessing.Manager().list()
python
import time
import random
class ActivePool:
def __init__(self, active_list, lock):
self.active = active_list
self.lock = lock
def makeActive(self, name):
with self.lock:
self.active.append(name)
def makeInactive(self, name):
with self.lock:
self.active.remove(name)
def __str__(self):
with self.lock:
return str(self.active)
def worker(s, pool):
name = multiprocessing.current_process().name
with s:
pool.makeActive(name)
print('Activating {} now running {}'.format(name, pool))
time.sleep(random.random())
pool.makeInactive(name)
if __name__ == '__main__':
mgr = multiprocessing.Manager()
active_list = mgr.list()
lock = multiprocessing.Lock()
pool = ActivePool(active_list, lock)
s = multiprocessing.Semaphore(3)
jobs = [
multiprocessing.Process(
target=worker,
name=str(i),
args=(s, pool),
)
for i in range(10)
]
for j in jobs:
j.start()
while True:
alive = 0
for j in jobs:
if j.is_alive():
alive += 1
j.join(timeout=0.1)
if alive > 0:
print('Now running {}'.format(pool))
time.sleep(0.1)
else:
break
>>> 输出结果:
Activating 1 now running ['1']
Activating 0 now running ['1', '0']
Activating 9 now running ['1', '0', '9']
Activating 5 now running ['1', '9', '5']
Activating 7 now running ['9', '5', '7']
Activating 4 now running ['5', '7', '4']
Now running ['5', '7', '4']
Activating 3 now running ['5', '4', '3']
Activating 6 now running ['4', '3', '6']
Now running ['4', '3', '6']
Activating 2 now running ['3', '6', '2']
Activating 8 now running ['6', '2', '8']
Now running ['6', '8']
Now running ['8']
Now running []
2.2 共享字典:multiprocessing.Manager().dict()
python
def worker(d, key, value):
d[key] = value
if __name__ == '__main__':
mgr = multiprocessing.Manager()
d = mgr.dict()
jobs = [
multiprocessing.Process(
target=worker,
args=(d, i, i * 2),
)
for i in range(10)
]
for j in jobs:
j.start()
for j in jobs:
j.join()
print('Results:', d)
>>> 输出结果:
Results: {0: 0, 2: 4, 1: 2, 4: 8, 6: 12, 3: 6, 5: 10, 8: 16, 7: 14, 9: 18}
2.3 共享命名空间:multiprocessing.Manager().Namespace()
注意,增加到Namespace的所有命名值对所有接收Namespace实例的进程都可见。
python
def producer(ns, event):
ns.value = 'This is the value'
event.set()
def consumer(ns, event):
try:
print('Before event: {}'.format(ns.value))
except Exception as err:
print('Before event, error:', str(err))
event.wait()
print('After event:', ns.value)
if __name__ == '__main__':
mgr = multiprocessing.Manager()
namespace = mgr.Namespace()
event = multiprocessing.Event()
p = multiprocessing.Process(
target=producer,
args=(namespace, event),
)
c = multiprocessing.Process(
target=consumer,
args=(namespace, event),
)
c.start()
p.start()
c.join()
p.join()
>>> 输出结果:
Before event, error: 'Namespace' object has no attribute 'value'
After event: This is the value
但是,更改命名空间中可变类型的元素内容并不会在其他进程中自动更新。如果想要有效更新变类型的内容,需要将它重新赋值给命名空间对象。
python
def producer(ns, event):
ns.my_list.append('This is the value')
event.set()
def consumer(ns, event):
print('Before event:', ns.my_list)
event.wait()
print('After event :', ns.my_list)
if __name__ == '__main__':
mgr = multiprocessing.Manager()
namespace = mgr.Namespace()
namespace.my_list = []
event = multiprocessing.Event()
p = multiprocessing.Process(
target=producer,
args=(namespace, event),
)
c = multiprocessing.Process(
target=consumer,
args=(namespace, event),
)
c.start()
p.start()
c.join()
p.join()
>>> 输出结果:
Before event: []
After event : []
十一、进程池:管理固定数目的工作进程
1、创建进程池:multiprocessing.Pool(processes, initializer, maxtasksperchild)
processes:指定进程池中的子进程数,默认为 cpu 核心数;initializer:每个子进程启动时执行的初始化函数;maxtasksperchild:每个子进程最多能执行的任务数。
子进程在完成maxtasksperchild所设定数量的任务后,会关闭当前子进程并启动一个新的子进程(即使没有更多工作要做,也会重新启动),这样可以避免长时间运行的工作进程消耗更多的系统资源
2、进程池对象.map(func, iterable, chunksize)
将可迭代对象iterable分片,分配给子进程执行函数func,返回保持顺序的结果列表。
chunksize参数:每个进程单次处理的数据量,用于性能调优。
3、进程池对象.close():关闭进程池,禁止提交新任务
注意,已提交的任务会继续执行。
4、进程池对象.join():等待所有子进程完成任务
注意,该方法必须在close() / terminate()后调用。
python
def do_calculation(data):
return data * 2
def start_process():
print('Starting', multiprocessing.current_process().name)
if __name__ == '__main__':
inputs = list(range(10))
print('Input :', inputs)
builtin_outputs = map(do_calculation, inputs)
print('Built-in:', builtin_outputs)
pool_size = multiprocessing.cpu_count() * 2
pool = multiprocessing.Pool(
processes=pool_size,
initializer=start_process,
maxtasksperchild=2
)
pool_outputs = pool.map(do_calculation, inputs)
pool.close() # no more tasks
pool.join() # wrap up current tasks
print('Pool :', pool_outputs)
>>> 输出结果:
Input : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Built-in: <map object at 0x10278cfa0>
Starting SpawnPoolWorker-1
Starting SpawnPoolWorker-3
Starting SpawnPoolWorker-5
Starting SpawnPoolWorker-6
Starting SpawnPoolWorker-2
Starting SpawnPoolWorker-4
Starting SpawnPoolWorker-8
Starting SpawnPoolWorker-7
Starting SpawnPoolWorker-9
Starting SpawnPoolWorker-10
Pool : [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
十二、实现单服务器 MapReduce
在基于 MapReduce 的系统中,输入数据会被拆分为多个数据块,交由不同的工作进程实例处理。每个输入数据块会通过一次简单的转换操作,被映射为一种中间状态。随后,这些中间数据会被汇总,并根据键值进行分区,从而让所有相关联的值归到同一组。最终,这些经过分区的数据会被归约为一个结果集。
python
import collections
import itertools
import operator
import string
import glob
class SimpleMapReduce:
def __init__(self, map_func, reduce_func, num_workers=None):
"""
参数说明:
- map_func:自定义映射函数,输入单个数据,返回 (key, value) 元组
- reduce_func:自定义归约函数,输入 (key, [value1, value2,...]),返回归约结果
- num_workers:进程池大小,默认=CPU核心数
"""
self.map_func = map_func # 保存映射函数
self.reduce_func = reduce_func # 保存归约函数
self.pool = multiprocessing.Pool(num_workers) # 创建进程池:num_workers=None 时,默认用 multiprocessing.cpu_count() 个进程
def partition(self, mapped_values):
"""
输入:mapped_values → 所有Map阶段返回的 (key, value) 键值对的迭代器
输出:[(key1, [v1, v2]), (key2, [v3, v4]), ...] → 按key分组后的结果
"""
partitioned_data = collections.defaultdict(list)
for key, value in mapped_values:
partitioned_data[key].append(value) # 相同key的value加入同一列表
# items() 返回 (key, [value_list]) 的元组列表,供Reduce阶段使用
return partitioned_data.items()
def __call__(self, inputs, chunksize=1):
"""
触发MapReduce执行,参数:
- inputs:输入数据的可迭代对象(如列表、生成器)
- chunksize:Map阶段每个进程单次处理的数据量(调优性能用)
"""
# ========== 1. Map阶段:并行执行map_func ==========
# pool.map:将inputs分片,并行执行self.map_func,返回所有映射结果的列表
# 示例:inputs=[1,2,3],map_func返回(x, x*2) → map_responses=[(1,2), (2,4), (3,6)]
map_responses = self.pool.map(
self.map_func,
inputs,
chunksize=chunksize, # 此示例中表示每次只处理一个文件
)
# ========== 2. Partition阶段:扁平化+按key分组 ==========
# itertools.chain(*map_responses):扁平化嵌套列表(若map_func返回列表,需此操作)
# 示例:map_responses=[[(a,1), (b,2)], [(a,3)]] → chain后为 (a,1), (b,2), (a,3)
partitioned_data = self.partition(
itertools.chain(*map_responses)
)
# ========== 3. Reduce阶段:并行执行reduce_func ==========
# pool.map:将分组后的 (key, value_list) 分片,并行执行reduce_func
reduced_values = self.pool.map(
self.reduce_func,
partitioned_data,
)
return reduced_values # 返回最终归约结果
def file_to_words(filename):
"""读取文件,返回 (单词, 1) 形式的键值对列表(Map阶段核心逻辑)"""
# 停用词集合:过滤无意义的高频虚词(如a/an/the等)
STOP_WORDS = set([
'a', 'an', 'and', 'are', 'as', 'be', 'by', 'for', 'if',
'in', 'is', 'it', 'of', 'or', 'py', 'rst', 'that', 'the',
'to', 'with',
])
# 创建标点符号映射字典:将所有标点替换为空格(便于拆分单词)
TR = str.maketrans({
p: ' '
for p in string.punctuation # string.punctuation包含所有标点(!@#$%^&*()等)
})
# 打印当前进程名称+正在读取的文件(便于观察多进程执行情况)
print('{} reading {}'.format(multiprocessing.current_process().name, filename))
output = [] # 存储 (word, 1) 键值对
# 打开文件并逐行处理
with open(filename) as f:
for line in f:
# 跳过以..开头的注释行(rst文件的注释格式)
if line.lstrip().startswith('..'):
continue
line = line.translate(TR) # 替换所有标点为空格
for word in line.split(): # 按空格拆分单词
word = word.lower() # 统一转为小写(避免大小写重复统计,如Python/python)
# 过滤条件:仅保留纯字母单词 + 非停用词
if word.isalpha() and word not in STOP_WORDS:
output.append((word, 1)) # 符合条件的单词标记为 (单词, 1)
return output
def count_words(item):
"""归约阶段核心逻辑:对同一单词的所有(1,1,...)求和,返回(单词, 总次数)"""
word, occurences = item # item是Partition阶段的结果:(单词, [1,1,...])
return (word, sum(occurences)) # 求和得到总出现次数
if __name__ == '__main__':
# 1. 获取当前目录下所有.rst格式的文件(输入数据源)
input_files = glob.glob('*.rst')
# 2. 创建MapReduce实例:指定Map函数和Reduce函数
mapper = SimpleMapReduce(file_to_words, count_words)
# 3. 执行MapReduce:传入文件列表,返回所有单词的(单词, 总次数)列表
word_counts = mapper(input_files)
# 4. 按词频降序排序
word_counts.sort(key=operator.itemgetter(1)) # 先按词频升序
word_counts.reverse() # 反转为降序
# 5. 输出前20个高频单词
print('\nTOP 20 WORDS BY FREQUENCY\n')
top20 = word_counts[:20]
# 计算前20个单词中最长的长度(用于格式化输出,对齐显示)
longest = max(len(word) for word, _ in top20)
for word, count in top20:
# 格式化输出:单词左对齐,长度=最长单词+1;次数占5位
print('{word:<{len}}: {count:5}'.format(
len=longest + 1,
word=word,
count=count)
)