进程

前言

  • 这一篇讲 multiprocessing:怎么开进程、Windows 上「启动方式」带来的坑、进程锁、进程池、进程间通信(Pipe / Queue)、共享状态(Manager / Value / Array / shared_memory),以及自定义进程类。
  • 所有示例在本机 Python 3.14.10(Windows)上实跑过,输出原样贴在代码块下面。为了让输出可比对,示例里不直接打印 pid,而是打印「pid 是否不同」这类稳定结论。

进程简介

  • 进程:操作系统进行资源分配的基本单位,一个进程有自己独立的地址空间(地址空间 = 这套进程独占的一整片内存编号,别的进程看不见也改不了里面的东西)。在 Python 里,一个进程对应一个解释器实例、一份独立的内存。
  • 线程:CPU 调度的基本单位,同进程内的线程共享地址空间(细节见《线程》)。
  • 单核怎么「多任务」:把 CPU 时间切成小片轮流分配给任务,切换足够快,人眼看不出而已。
  • 多核:任务数不超过核心数时可以真正并行;实际系统里任务远多于核心,仍然是轮转,只是「干活的人」多了几个。
  • 为什么要用多进程而不是多线程:CPython 有 GIL(全局解释器锁,见《线程》),纯 Python 字节码(源码先编译成的中间指令,解释器再逐条执行)执行同一时刻只有一个线程在跑;要吃满多核,计算密集型任务得用多进程(或换解释器 / C 扩展释放 GIL,3.13 起还有实验性的 free-threaded 构建)。
  • 一个进程能看到的 CPU 时间片由操作系统决定,Python 只负责创建和管理进程对象。

Process 的基本用法

Process(target=函数, args=(...), kwargs={...}, name=...) 只是创建对象,要 start() 才真正起进程;join() 等它结束。

Process() 的签名是 Process(group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None),参数逐个说:

  • target:子进程要执行的函数对象,写函数名不要加括号(child 而不是 child())。不传也行,子进程空跑一趟、exitcode 仍是 0。
    • target 的函数必须定义在模块顶层:Windows 的 spawn 要把目标函数 pickle 后带过去,lambda 会 PicklingError: Can't pickle local object,写在 if __name__ 里的嵌套函数会在子进程报 AttributeError: module '__mp_main__' has no attribute '函数名'。
  • args:传给 target 的位置参数,必须是元组,默认 ()。只有一个参数也得带逗号:args=(main_pid,);写成 args=(1) 会在 start() 当场 TypeError: 'int' object is not iterable。
  • kwargs:传给 target 的关键字参数,字典,默认 None(当作 {})。args / kwargs 里的每个值都要能被 pickle,连接对象、锁句柄这类传不过去,得改用 Queue / Manager(见后面「进程间通信」)。
  • name:进程名,纯用于辨认——p.name、multiprocessing.active_children()、日志里靠它区分是谁。
  • daemon:* 之后的关键字限定参数,只能写成 daemon=True,等价于创建后再设 p.daemon = True。含义是「父进程退出时我被一起带走」,详见下面守护进程那节;只能在 start() 之前设,启动之后改会 AssertionError: process has already started。
  • group:历史遗留参数,必须留 None(Python 3.9 起废弃),传别的值直接 AssertionError: group argument must be None for now。

默认名 Process-N 里的 N 按创建顺序递增,手动命名的也占号——下面这段里先建了两个对象,所以后面两个默认名是 Process-3、Process-4,它和 pid 没有任何关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import inspect
import os
from multiprocessing import Process


def noop():
pass


if __name__ == '__main__':
print('签名:', inspect.signature(Process.__init__))

p = Process()
print('默认 name:', p.name, '| daemon:', p.daemon, '| 未 start 的 pid:', p.pid)
p.start()
p.join()
print('不传 target 也正常结束,exitcode:', p.exitcode)

p2 = Process(target=noop, name='worker-2')
print('传了 name:', p2.name)
p2.name = '改名了'
print('name 可写:', p2.name)
print('默认名按创建顺序:', Process(target=noop).name, Process(target=noop).name)

try:
Process(target=noop, args=(1)).start()
except TypeError as e:
print('args 少逗号:', type(e).__name__, e)

try:
Process(group=1)
except AssertionError as e:
print('group 传值:', type(e).__name__, e)

输出:

1
2
3
4
5
6
7
8
签名: (self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None)
默认 name: Process-1 | daemon: False | 未 start 的 pid: None
不传 target 也正常结束,exitcode: 0
传了 name: worker-2
name 可写: 改名了
默认名按创建顺序: Process-3 Process-4
args 少逗号: TypeError 'int' object is not iterable
group 传值: AssertionError group argument must be None for now
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import time
from multiprocessing import Process

def child(main_pid):
print('子进程里 __name__ =', __name__)
print('子进程 pid 与主进程不同:', os.getpid() != main_pid)
print('子进程看到的父进程就是主进程:', os.getppid() == main_pid)
time.sleep(0.2)
print('子进程结束')

if __name__ == '__main__':
main_pid = os.getpid()
print('主进程 pid 是正整数:', main_pid > 0)
p = Process(target=child, args=(main_pid,), name='worker-1')
print('start 前 is_alive:', p.is_alive())
p.start()
p.join() # 等子进程结束
print('join 后 is_alive:', p.is_alive(), '| exitcode:', p.exitcode)
print('主进程结束')

输出:

1
2
3
4
5
6
7
8
主进程 pid 是正整数: True
start 前 is_alive: False
子进程里 __name__ = __mp_main__
子进程 pid 与主进程不同: True
子进程看到的父进程就是主进程: True
子进程结束
join 后 is_alive: False | exitcode: 0
主进程结束

start() 和 join() 各自到底做了什么(这两个名字最容易「记住叫法、没记住行为」):

  • start():向操作系统申请创建子进程,并让新进程从 target 指定的那个函数开始跑。它是「开始」不是「跑完」——调用完立刻返回,父进程继续执行下一行,于是父子并发,谁先打印不固定。同一个对象只能 start() 一次,重复调用报 AssertionError: cannot start a process twice。
  • join():让调用它的一方(这里是父进程)停在原地等,等那个子进程结束才往下走。名字容易误解成「把两个进程合并」,实际意思是「我等它」。返回值永远是 None;join(0.2) 是「最多等 0.2 秒」,到点就返回(可能还没等完,要配 is_alive() 判断);没 start() 就 join() 报 AssertionError: can only join a started process;重复 join() 不报错。
  • join() 等的是它那一个,不是「最先跑完的那个」:下面 B 先结束,p1.join() 仍然要等 A 跑完才返回。
  • 等待期间父进程是挂起的(不是空转轮询),不占 CPU。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import time
from multiprocessing import Process


def child(tag, sec):
time.sleep(sec)
print('子进程', tag, '跑完', flush=True)


def noop():
pass


if __name__ == '__main__':
p1 = Process(target=child, args=('A', 0.6))
p2 = Process(target=child, args=('B', 0.2))
p1.start()
p2.start()
print('两个 start 都调用完了,主进程立刻走到下一行', flush=True)
p1.join()
print('p1.join() 这时才返回(B 早就跑完了,等的是 A)', flush=True)
print('p2.join() 的返回值:', p2.join())

q = Process(target=noop)
try:
q.join()
except AssertionError as e:
print('没 start 就 join ->', type(e).__name__, e)
q.start()
try:
q.start()
except AssertionError as e:
print('重复 start ->', type(e).__name__, e)
q.join()
print('重复 join 不报错,返回值:', q.join())

输出:

1
2
3
4
5
6
7
8
两个 start 都调用完了,主进程立刻走到下一行
子进程 B 跑完
子进程 A 跑完
p1.join() 这时才返回(B 早就跑完了,等的是 A)
p2.join() 的返回值: None
没 start 就 join -> AssertionError can only join a started process
重复 start -> AssertionError cannot start a process twice
重复 join 不报错,返回值: None

几个要点:

  • start() 之后父进程和子进程并发执行,谁先打印不固定;join() 才把顺序钉住。
  • join() 的返回值永远是 None,它只负责等;带超时 join(0.2) 到点就返回,此时子进程可能还没结束,所以要配 is_alive() 判断有没有等完。
  • is_alive():start() 之前 False,运行中 True,join() 之后 False。
  • pid:操作系统进程号,start() 之前是 None。
  • exitcode:None 表示还在跑,0 正常结束,os._exit(3) 就是 3。
  • 子进程里抛未捕获异常时 exitcode 是 1,traceback 只打在子进程自己的 stderr,父进程看不到——只能靠这个 1 发现出过事。
  • terminate() 不等了直接终止子进程,之后仍要 join() 收尾;exitcode 记成 -15,Windows 上没有真信号,multiprocessing 用 SIGTERM 的编号表示「被终止」。
  • 传参一律走 args / kwargs,参数会被 pickle 后带进子进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
import time
from multiprocessing import Process


def noop():
pass


def boom():
os._exit(3)


def nap():
time.sleep(30)


if __name__ == '__main__':
for fn, label in [(None, '不传 target'), (noop, 'target 正常返回'),
(boom, 'os._exit(3)')]:
p = Process(target=fn) if fn else Process()
p.start()
p.join()
print(f'{label:14} exitcode = {p.exitcode}')

p = Process(target=nap)
p.start()
print('join(0.2) 返回:', p.join(0.2), '| 返回后 is_alive:', p.is_alive())
p.terminate()
p.join()
print('terminate 后 exitcode =', p.exitcode, '| is_alive:', p.is_alive())

输出:

1
2
3
4
5
不传 target      exitcode = 0
target 正常返回 exitcode = 0
os._exit(3) exitcode = 3
join(0.2) 返回: None | 返回后 is_alive: True
terminate 后 exitcode = -15 | is_alive: False

启动方式与「全局变量不共享」

  • 三种启动方式

    • spawn 全新起一个解释器并重新导入主模块。
    • fork 直接复制父进程内存(Unix)。
    • forkserver 由一个干净的服务进程派生(Unix)。Windows 和 macOS 上只有 spawn。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import multiprocessing
    import os

    VAR = 100

    def show():
    # spawn 起来的子进程会重新导入主模块,VAR 在子进程里是「刚初始化」的值
    print('子进程看到的 VAR =', VAR)

    if __name__ == '__main__':
    print('当前启动方式:', multiprocessing.get_start_method())
    print('可用的启动方式:', sorted(multiprocessing.get_all_start_methods()))
    VAR = 999 # 主进程里改了,子进程看不到
    p = multiprocessing.Process(target=show)
    p.start()
    p.join()
    print('主进程里的 VAR =', VAR)

    输出:

    1
    2
    3
    4
    当前启动方式: spawn
    可用的启动方式: ['spawn']
    子进程看到的 VAR = 100
    主进程里的 VAR = 999

    子进程打印的 VAR 是 100 而不是 999:因为 spawn 会重新导入主模块,模块级代码又执行了一遍 VAR = 100,而 if __name__ == '__main__': 那块在子进程里不会执行。

    这也解释了「为什么必须把启动代码写进 if __name__ == '__main__':」——不写的话,子进程重新导入时又会执行一遍 start(),无限套娃(RuntimeError 或直接把机器打满)。

  • 传值要用参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import os
    from multiprocessing import Process

    def child(main_pid):
    # 想跨进程传值就用参数(会被 pickle 后带过去)
    print('通过参数拿到主进程 pid:', os.getpid() != main_pid)

    if __name__ == '__main__':
    p = Process(target=child, args=(os.getpid(),))
    p.start()
    p.join()

    输出:

    1
    通过参数拿到主进程 pid: True
  • 全局变量改不动对方

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import multiprocessing

    num = 250
    lt = ['hello']

    def child():
    global num, lt
    num += 10
    lt.append('world')
    print('子进程里:', num, lt)

    if __name__ == '__main__':
    print('主进程改之前:', num, lt)
    p = multiprocessing.Process(target=child)
    p.start()
    p.join()
    print('主进程改之后:', num, lt, '<- 子进程的修改不会回来')

    输出:

    1
    2
    3
    主进程改之前: 250 ['hello']
    子进程里: 260 ['hello', 'world']
    主进程改之后: 250 ['hello'] <- 子进程的修改不会回来

    num 是 int、lt 是 list,两种都一样:子进程拿到的是副本,改完不会回传。想共享得用下一节的机制。

守护进程与子进程异常

  • daemon = True(或 Process(..., daemon=True))表示「父进程一退出我就被带走」;不设的话,Python 进程退出时会等待非守护子进程结束。
  • 「守护」这个词容易让人以为它在替谁值班——其实谁也没守护。它来自 Unix 里长期在后台跑的服务程序(daemon,如 sshd、mysqld),这类程序的共同点是「依附于上级,上级走了它就该消失」。Python 借这个词表达的就是这层依附关系:守护子进程不要求收尾,父进程退出时它被直接终止。
  • 两条实测出来的规矩:
    • 守护进程不许再生子进程:在里面 Process(...).start() 会抛 AssertionError: daemonic processes are not allowed to have children(想套娃就让中间那层非守护)。
    • 被带走是硬终止,finally 不保证执行:实测守护子进程只打印了「开始」,finally 那行没出现;同一份代码把 daemon=True 改成 False,finally 就正常执行了。所以「一定要写完」的活别交给守护进程,要么非守护 + join,要么用 Pool 的 close() + join() 收尾。
  • 一个很自然的误解:看到「守护」两个字,容易理解成「它守护着孩子——主进程退出了,子进程被保护着继续跑完」。实际规则正相反,daemon=True 是「孩子跟着大人走」。
  • 名字为什么这么绕:Unix 的 daemon 指「脱离终端、长期在后台活着的服务进程」;Python 的 daemon=True 指「依附上级,上级走了我就不必收尾」。同一个词,生命周期规则正好相反——这也是最容易记反的地方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
from multiprocessing import Process

def long_job():
print('守护进程开始', flush=True)
time.sleep(5)
print('这行不会被打印出来', flush=True)

if __name__ == '__main__':
p = Process(target=long_job, daemon=True)
p.start()
time.sleep(1.5) # 等子进程真的跑起来
print('此时守护进程还活着:', p.is_alive())
print('主进程结束,不等它')

输出:

1
2
3
守护进程开始
此时守护进程还活着: True
主进程结束,不等它

主进程 1.5 秒后就走,long_job 里的 time.sleep(5) 等不到,第二句不会被打印——这就是 daemon 的语义。两个细节:示例里的 print(..., flush=True) 不是可有可无,子进程被强杀时缓冲区里的输出会丢;反过来,daemon 进程不能再创建子进程(daemonic processes are not allowed to have children)。

子进程里抛异常只会结束它自己:

1
2
3
4
5
6
7
8
9
10
11
12
13
from concurrent.futures import ProcessPoolExecutor

def boom():
raise ValueError('子进程里抛异常')

if __name__ == '__main__':
with ProcessPoolExecutor(max_workers=1) as ex:
fut = ex.submit(boom)
try:
fut.result() # 异常被送回主进程
except ValueError as e:
print('主进程收到子进程的异常:', e)
print('主进程继续跑完')

输出:

1
2
主进程收到子进程的异常: 子进程里抛异常
主进程继续跑完

(子进程的 traceback 会先打到 stderr,被 join() 收回来的是退出码。)想拿到异常内容,用 concurrent.futures.ProcessPoolExecutor 更省事——它把异常原样抛回父进程。

进程锁

多个进程写同一个资源(共享计数、同一个文件)会互相踩,用 Lock 把临界区串行化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import multiprocessing
import time

def work(idx, lock, counter):
time.sleep(0.02 * (5 - idx)) # 故意让完成顺序打乱
with lock: # 同一时刻只有一个进程能进这段
counter.value += 1
print('任务', idx, '完成,当前计数', counter.value)

if __name__ == '__main__':
lock = multiprocessing.Lock()
counter = multiprocessing.Value('i', 0)
ps = [multiprocessing.Process(target=work, args=(i, lock, counter)) for i in range(5)]
for p in ps:
p.start()
for p in ps:
p.join()
print('最终计数 =', counter.value)

输出:

1
2
3
4
5
6
任务 4 完成,当前计数 1
任务 3 完成,当前计数 2
任务 2 完成,当前计数 3
任务 1 完成,当前计数 4
任务 0 完成,当前计数 5
最终计数 = 5
  • 计数一路 1→5 不跳号,说明每次 += 1 都在独占状态下完成;不加锁时「读-改-写」可能被别的进程插队,最终结果就会小于 5。
  • with lock: 会自动 acquire / release,比手写 lock.acquire() 安全(中途抛异常也能释放)。
  • 还有 RLock(可重入)、Semaphore(限流)、Condition(等待/通知),用法和 threading 里的同名对象一致,只是跨进程。
  • multiprocessing.Lock 的接口和 threading.Lock 一样(acquire(blocking=True, timeout=-1) / release()),但有一条必须记住的差别:它得作为参数传给子进程(Process(target=work, args=(lock, counter))),不能像线程那样靠全局变量拿——子进程是独立内存,全局变量在那边是重新导入出来的另一份。Pool 场景里则要用 Pool(initializer=..., initargs=(lock,)) 传进去。
  • 写文件更常见的做法是「各写各的临时文件,最后合并」,锁只适合临界区很短的场景。

进程池

任务多、要控制并发数时,用 Pool 而不是手搓一堆 Process。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import multiprocessing
import time

def task(n):
time.sleep(0.05)
return n * n

if __name__ == '__main__':
print('CPU 核心数是正整数:', multiprocessing.cpu_count() > 0)
with multiprocessing.Pool(4) as pool:
print('map 结果(按入参顺序):', pool.map(task, range(8)))
print('imap 结果:', list(pool.imap(task, range(4))))
res = pool.apply_async(task, (9,))
print('apply_async 返回:', type(res).__name__)
print('get() 取结果:', res.get(timeout=30))
print('AsyncResult.successful():', res.successful())
print('退出 with 之后任务全部完成')

输出:

1
2
3
4
5
6
7
CPU 核心数是正整数: True
map 结果(按入参顺序): [0, 1, 4, 9, 16, 25, 36, 49]
imap 结果: [0, 1, 4, 9]
apply_async 返回: ApplyResult
get() 取结果: 81
AsyncResult.successful(): True
退出 with 之后任务全部完成
  • map 会按入参顺序返回结果,即使完成顺序不同。
  • imap 是迭代器(不一次性把全部结果装进内存,而是每次要下一个时才现算),边算边给,适合结果很大或想早点开始处理。
  • apply_async 返回 AsyncResult,用 .get(timeout=...) 取值;一定要给 timeout,否则子进程卡死时父进程会永远等下去。
  • 池大小一般不超过 cpu_count();with Pool(n) as pool 退出时会自动 terminate,正常收尾仍推荐 close() + join()。
1
2
3
4
5
6
7
8
9
10
11
12
import multiprocessing

def to_upper(s):
return s.upper()

if __name__ == '__main__':
words = ['alpha', 'beta', 'gamma', 'delta']
with multiprocessing.Pool(2) as pool:
print('map 保序:', pool.map(to_upper, words))
print('map 分块:', pool.map(to_upper, words, chunksize=2))
print('starmap 多参数:', pool.starmap(pow, [(2, 3), (3, 2)]))
print('map_async 再 get:', pool.map_async(to_upper, words).get(timeout=30))

输出:

1
2
3
4
map 保序: ['ALPHA', 'BETA', 'GAMMA', 'DELTA']
map 分块: ['ALPHA', 'BETA', 'GAMMA', 'DELTA']
starmap 多参数: [8, 9]
map_async 再 get: ['ALPHA', 'BETA', 'GAMMA', 'DELTA']

chunksize 决定「一次给每个进程派多少个任务」:任务多且每个很轻时调大能省 IPC 开销;任务少且重就用默认。

回调写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import multiprocessing

def task(n):
return n + 100

if __name__ == '__main__':
done_list = []

def done(result): # 回调在父进程里执行
done_list.append(result)

pool = multiprocessing.Pool(2)
for i in range(3):
pool.apply_async(task, (i,), callback=done)
pool.close() # 不再接受新任务
pool.join() # 等已提交的任务全部完成
print('回调收集到(排序后):', sorted(done_list))
print('主进程结束')

输出:

1
2
回调收集到(排序后): [100, 101, 102]
主进程结束

回调在父进程里执行,参数是任务函数的返回值;三个回调的先后顺序不固定(取决于谁先算完),但 close() + join() 能保证它们都在「主进程结束」之前跑完。

进程间通信:Pipe 与 Queue

  • Pipe:两端直连

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import multiprocessing

    def child(conn):
    data = conn.recv()
    print('子进程收到:', data)
    conn.send([x * 2 for x in data])

    if __name__ == '__main__':
    parent, child_end = multiprocessing.Pipe()
    p = multiprocessing.Process(target=child, args=(child_end,))
    p.start()
    parent.send([1, 2, 3])
    print('主进程收到回执:', parent.recv())
    p.join()
    read_end, write_end = multiprocessing.Pipe(duplex=False)
    write_end.send('半双工')
    print('duplex=False 读端拿到:', read_end.recv())
    print('写端还有 recv 方法吗:', hasattr(write_end, 'recv'))
    try:
    write_end.recv()
    except OSError as e:
    print('真的去读会:', type(e).__name__, e)

    输出:

    1
    2
    3
    4
    5
    子进程收到: [1, 2, 3]
    主进程收到回执: [2, 4, 6]
    duplex=False 读端拿到: 半双工
    写端还有 recv 方法吗: True
    真的去读会: OSError connection is write-only
    • 默认全双工,两端都能收发。
    • duplex=False 时 conn1 只能读、conn2 只能写(注意 recv 方法仍然存在,调用才会报错,所以 hasattr 是 True)。适合一对一。
    • recv() 在对端没发之前会阻塞(就是卡在这一行不返回,后面的代码一行都走不下去),两端都等就死锁。
  • Queue:多对多的任务分发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import multiprocessing

    def consumer(q, out, tag):
    got = [q.get() for _ in range(3)] # 队列空时这里会阻塞
    out.extend(got)

    if __name__ == '__main__':
    q = multiprocessing.Queue(3) # maxsize=3:放满之后 put 会等
    print('刚创建时 empty:', q.empty())
    shared = multiprocessing.Manager().list()
    c1 = multiprocessing.Process(target=consumer, args=(q, shared, 'A'))
    c2 = multiprocessing.Process(target=consumer, args=(q, shared, 'B'))
    c1.start()
    c2.start() # 先让消费者就位,再投喂
    for i in range(6):
    q.put(i)
    c1.join()
    c2.join()
    print('两个消费者合起来取到:', sorted(shared))
    print('取空之后 empty:', q.empty())
    q.cancel_join_thread()
    q.close()

    输出:

    1
    2
    3
    刚创建时 empty: True
    两个消费者合起来取到: [0, 1, 2, 3, 4, 5]
    取空之后 empty: True

    每个元素只会被一个消费者取走,所以两个消费者合起来正好拿到 0~5。要点:

    • put / get 走的是后台线程 + 管道,qsize()、empty()、full() 只是近似值,别拿它们做严格判断。
    • 队列空时 get() 会阻塞,所以要么放「毒丸」(None)通知退出,要么用 get(timeout=...)。
    • 进程结束前记得 cancel_join_thread() / close(),否则 join() 可能挂住。
    • 需要「生产者-消费者 + 有界缓冲」时,Queue(maxsize=n) 就是背压:满了 put 就等。

共享状态:Manager 与 shared_memory

  • Manager:什么都能共享,但慢

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import multiprocessing

    def child(v, s, a, lst, dct):
    v.value += 10
    s.value = b'world'
    a[0] = 5
    lst.append('大哥,您收好!')
    dct['name'] = 'xiaoming'

    if __name__ == '__main__':
    server = multiprocessing.Manager()
    v = server.Value('i', 250)
    s = server.Value('s', b'hello')
    a = server.Array('i', range(5))
    lst = server.list()
    dct = server.dict()
    p = multiprocessing.Process(target=child, args=(v, s, a, lst, dct))
    p.start()
    p.join()
    print('主进程看到 v =', v.value)
    print('主进程看到 s =', s.value)
    print('主进程看到 a[0] =', a[0])
    print('主进程看到 list =', list(lst))
    print('主进程看到 dict =', dict(dct))
    server.shutdown()

    输出:

    1
    2
    3
    4
    5
    主进程看到 v = 260
    主进程看到 s = b'world'
    主进程看到 a[0] = 5
    主进程看到 list = ['大哥,您收好!']
    主进程看到 dict = {'name': 'xiaoming'}

    Manager() 起一个服务进程,Value / Array / list / dict 都是代理对象,每次读写都在走 IPC——所以它方便但慢,而且 lst.append() 这种修改对代理是有效的(和「全局变量不共享」正好相反)。
    Value('i', 250) 的类型码来自 ctypes:i 是 int、f 是 float、s 是 c_char_p(要传 bytes)。

  • 不加 Manager 的共享变量

    multiprocessing.Value / Array 直接放在共享内存里,比 Manager 快得多;需要原子性时给它配锁:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import multiprocessing

    def worker(v, n):
    for _ in range(n):
    with v.get_lock(): # Value 自带一把锁
    v.value += 1

    if __name__ == '__main__':
    v = multiprocessing.Value('i', 0)
    ps = [multiprocessing.Process(target=worker, args=(v, 1000)) for _ in range(4)]
    for p in ps:
    p.start()
    for p in ps:
    p.join()
    print('4 个进程各加 1000 次,结果 =', v.value)

    输出:

    1
    4 个进程各加 1000 次,结果 = 4000
  • shared_memory:直接共享一大块字节

    3.8 加入,适合数组类大数据,避免「每次读写都走 IPC」。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import multiprocessing.shared_memory as shm

    block = shm.SharedMemory(create=True, size=16)
    print('块名是随机字符串:', len(block.name) > 8)
    view = memoryview(block.buf)[:8]
    view[0:4] = b'abcd'
    print('写入后读回:', bytes(block.buf[:4]))
    view.release()
    block.close()
    block.unlink()
    print('unlink 之后资源已释放')

    输出:

    1
    2
    3
    块名是随机字符串: True
    写入后读回: b'abcd'
    unlink 之后资源已释放

    它只是一块裸内存,需要自己配 numpy 或 ctypes 解释内容,也要记得 close() + unlink()(不 unlink 会残留到系统重启)。NumPy 数组跨进程共享就是 shared_memory + np.frombuffer 的组合。

自定义进程类

继承 Process 并实现 run(),start() 后自动执行;适合把「一个 worker 的状态和行为」封装起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time
from multiprocessing import Process

class MyProcess(Process):

def __init__(self, delay, times):
super().__init__()
self.delay = delay
self.times = times

def run(self): # start() 之后自动执行这个
for _ in range(self.times):
print('子进程运行中...')
time.sleep(self.delay)

if __name__ == '__main__':
p = MyProcess(0.05, 3)
p.start()
p.join()
print('自定义进程类跑了', p.times, '轮, exitcode =', p.exitcode)

输出:

1
2
3
4
子进程运行中...
子进程运行中...
子进程运行中...
自定义进程类跑了 3 轮, exitcode = 0

注意 run() 是在子进程里执行的,self.delay、self.times 这些属性是随对象一起被 pickle 过去的;在 run() 里改的属性不会回到父进程。

整体示例:进程池 + 锁 + 共享计数

把锁、Value、Manager().list()、进程池串成一个能验证的小任务分发器:三个耗时不同的任务,池里最多 2 个进程并发,用锁保证计数和记录不串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import multiprocessing
import time

LOCK = None
COUNTER = None
OUT = None

def init(lock, counter, out): # 池进程创建时注入共享对象
global LOCK, COUNTER, OUT
LOCK, COUNTER, OUT = lock, counter, out

def worker(name, seconds):
time.sleep(seconds) # 模拟耗时
with LOCK:
COUNTER.value += 1
OUT.append((COUNTER.value, name))

if __name__ == '__main__':
jobs = [('下载图片', 0.45), ('压缩日志', 0.05), ('发送通知', 0.25)]
lock = multiprocessing.Lock()
counter = multiprocessing.Value('i', 0)
manager = multiprocessing.Manager()
out = manager.list()

with multiprocessing.Pool(2, initializer=init, initargs=(lock, counter, out)) as pool:
pool.starmap(worker, jobs) # 最多 2 个进程并发

print('提交任务数:', len(jobs))
print('完成计数 :', counter.value)
print('完成顺序 :', [name for _, name in sorted(out)])
manager.shutdown()

输出:

1
2
3
提交任务数: 3
完成计数 : 3
完成顺序 : ['压缩日志', '发送通知', '下载图片']

读法:

  • Pool(2) 只有两个工人,三个任务分两批跑,所以总耗时接近「最慢的那批」而不是三个之和。
  • 完成顺序由睡眠时长决定:压缩日志(0.05) → 发送通知(0.10) → 下载图片(0.15)。
  • 计数用锁保护,out 用 Manager 列表收集,最后按序号排序还原完成顺序——这是「并发执行、有序汇报」的常见套路。

参考

本文结束 感谢您的阅读
正在加载今日诗词....