multiprocessing:创建并管理多个进程

文章目录

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.ProcesssetName() / 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.ProcesssetDaemon(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来配置日志器。

  1. 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()方法中包含blockingtimeout两个可选参数,用于控制获取锁的行为(是否阻塞、阻塞多久),默认无限期阻塞 。注意,只有在参数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)
        )
相关推荐
paradoxaaa_1 小时前
cusor无限续杯教程
python
wu_asia1 小时前
每日一练壹
算法
m5655bj2 小时前
通过 Python 删除 Excel 中的空白行列
python·ui·excel
程序员酥皮蛋2 小时前
hot 100 第二十二题 22.相交链表
数据结构·算法·leetcode·链表
全栈前端老曹2 小时前
【Redis】Redis 客户端连接与编程实践——Python/Java/Node.js 连接 Redis、实现计数器、缓存接口
前端·数据库·redis·python·缓存·全栈
一只小小的芙厨2 小时前
寒假集训·子集枚举2
c++·笔记·算法·动态规划
Y.O.U..2 小时前
力扣刷题-61.旋转链表
算法·leetcode·链表
这波不该贪内存的2 小时前
【无标题】
算法·排序算法
橙露2 小时前
排序算法可视化:用 Java 实现冒泡、快排与归并排序的对比分析
java·python·排序算法