Python Advance 📂 Multiprocess · 1 of 1 328 min read

Python Multiprocessing & Queues

Master Python's escape from the GIL. Learn multiprocessing.Process for true parallel execution, Pool for effortless work distribution (map, imap_unordered, starmap), IPC through Queue and Pipe, and blazing-fast data sharing via Value, Array, Manager, and the modern shared_memory module. Includes benchmarks, image-processing and NumPy examples, fork vs spawn internals, synchronisation primitives, pitfalls, and a golden-rules cheat sheet.

🌐
SectionSection第 01 节

The Story That Explains Multiprocessing 🏗️Multiprocessing समझाने वाली कहानी 🏗️一个讲清多进程的故事 🏗️

Building a House With One Contractor vs Fiveएक Contractor बनाम पाँच Contractors से घर बनाना用一个承包商还是五个承包商盖房子
Imagine you are building a house. With one contractor, work goes in sequence — foundation, framing, plumbing, wiring, drywall. You can hire assistants (threads), but they all share the contractor's single set of tools (the GIL). However many assistants you hire, only one saw runs at a time.

Now hire five independent contractors, each with their own crew, their own truck of tools and their own workspace. They really work in parallel — one lays the foundation while another frames, another wires, another plumbs. They only need to talk now and then (shared blueprints, material deliveries).

That is multiprocessing in Python. Each process is its own contractor, with its own Python interpreter, its own GIL and its own memory. True parallelism. The cost is coordination: passing messages between processes takes more work than passing them between threads.
मान लीजिए आप घर बना रहे हैं। एक contractor के साथ काम क्रम से होता है — नींव, ढाँचा, plumbing, wiring, drywall। आप सहायक (threads) रख सकते हैं, पर सब contractor के एक ही औज़ारों के सेट (GIL) को बाँटते हैं। कितने भी सहायक रखें, एक समय में एक ही आरी चलती है।

अब पाँच अलग contractors रखिए, हर एक की अपनी टीम, औज़ारों से भरा अपना truck और अपनी जगह। वे सच में parallel काम करते हैं — एक नींव डालता है, दूसरा ढाँचा बनाता है, तीसरा wiring, चौथा plumbing। उन्हें बस कभी-कभी बात करनी होती है (साझा नक़्शे, सामान की delivery)।

यही Python में multiprocessing है। हर process अपना contractor है, अपने Python interpreter, अपने GIL और अपनी memory के साथ। असली parallelism। क़ीमत है तालमेल: processes के बीच संदेश भेजना threads के बीच भेजने से ज़्यादा मेहनत का काम है।
想象你在盖房子。只有 一个承包商 时,工作按顺序进行 — 地基、框架、水管、电线、石膏板。你可以雇助手(线程),但他们都共用承包商唯一的一套工具(GIL)。不管雇多少助手,同一时间只有一把锯在工作。

现在雇 五个独立的承包商,每人都有自己的队伍、一卡车工具和自己的工作区。他们真正地并行工作 — 一个打地基,一个搭框架,一个布线,一个装水管。他们只需偶尔沟通(共享图纸、材料配送)。

这就是 Python 的 多进程。每个进程都是一个独立的承包商,有自己的 Python 解释器、自己的 GIL 和自己的内存。真正的并行。代价是协调:在进程之间传递消息比在线程之间传递更费事。
🧠
The Core Insightमूल बात核心要点

The GIL belongs to one interpreter, not to the whole machine. Start 8 processes and you get 8 interpreters, 8 GILs, and up to 8 cores running Python bytecode at the same time. This is Python's way around the GIL for CPU-bound work.

GIL एक interpreter का होता है, पूरी machine का नहीं। 8 processes शुरू कीजिए और आपको 8 interpreters, 8 GILs, और एक साथ Python bytecode चलाते 8 तक cores मिलते हैं। CPU-bound काम के लिए GIL से बचने का Python का यही रास्ता है।

GIL 属于单个解释器,而不是整台机器。启动 8 个进程,你就得到 8 个解释器、8 个 GIL,以及最多 8 个核心同时运行 Python 字节码。这是 Python 在 CPU 密集型工作中绕过 GIL 的方法。

▶️
Six Simulators and a Quizछह Simulators और एक Quiz六个模拟器和一个小测验

Look for the interactive boxes: Start-Method Lab (Section 05), Pool Lab (07), Pickle Checker (08), IPC Cost Lab (10), Shared-State Lab (11) and Bug Doctor (17). All their numbers come from real runs on our 2-core test machine.

Interactive डिब्बे खोजें: Start-Method Lab (Section 05), Pool Lab (07), Pickle Checker (08), IPC Cost Lab (10), Shared-State Lab (11) और Bug Doctor (17)। इनके सारे numbers हमारी 2-core test machine पर असली runs से हैं।

请留意这些互动框:启动方式实验室(第 05 节)、Pool 实验室(07)、Pickle 检查器(08)、进程间通信开销实验室(10)、共享状态实验室(11)和 Bug 医生(17)。所有数字都来自我们双核测试机上的真实运行。


SectionSection第 02 节

Threads vs Processes — The Key Difference ⚖️Threads बनाम Processes — मुख्य अंतर ⚖️线程 vs 进程 — 根本区别 ⚖️

Figure 1 — Memory Model: Threads Share, Processes Do Notचित्र 1 — Memory Model: Threads बाँटते हैं, Processes नहीं图 1 — 内存模型:线程共享,进程不共享
Memory model: threads share, processes do notONE PROCESS, 3 THREADSएक PROCESS, 3 THREADS一个进程,3 个线程shared memory + one GILT1T2T3cheap to share — but one runs Python at a timeबाँटना सस्ता — पर एक समय में एक Python चलाता है共享很便宜 — 但一次只有一个运行 PythonTHREE PROCESSESतीन PROCESSES三个进程P1own GILown memoryP2own GILown memoryP3own GILown memorytrue parallel — but data must be copied between themअसली parallel — पर data copy करना पड़ता है真正并行 — 但数据要在它们之间复制

Threads share one memory space and one GIL. Processes each have their own.

Threads एक memory space और एक GIL बाँटते हैं। हर process का अपना होता है।

线程共享一个内存空间和一个 GIL。每个进程都有自己的一份。

Aspectपहलू方面threadingmultiprocessing
MemoryMemory内存Sharedसाझा共享Separate per processहर process की अलग每个进程独立
Parallel on many coresकई cores पर parallel多核并行No (GIL) — except during I/Oनहीं (GIL) — I/O के दौरान छोड़कर否(GIL)— I/O 期间除外Yes — true parallelहाँ — असली parallel是 — 真正并行
Start-up costशुरू करने की क़ीमत启动开销~0.1 ms~0.1 ms约 0.1 毫秒~1–3 ms with fork, ~40 ms with spawn (our machine)fork से ~1–3 ms, spawn से ~40 ms (हमारी machine)fork 约 1–3 毫秒,spawn 约 40 毫秒(我们的机器)
Memory per unitहर unit की memory每个单元的内存Small (stack is reserved, not filled)कम (stack reserved, भरा नहीं)小(栈只是预留,并未用满)A whole interpreter — often 10+ MBपूरा interpreter — अक्सर 10+ MB一个完整解释器 — 常超过 10 MB
Sharing dataData बाँटना共享数据Free — just pass the objectमुफ़्त — बस object दे दें免费 — 直接传对象Pickled and copied, or shared memoryPickle होकर copy, या shared memory序列化后复制,或用共享内存
Typical bugsआम bugs典型 bugRace conditionsRace conditions竞态条件Pickling errors, lost updates in shared statePickling errors, साझा state में खोए updatesPickle 错误、共享状态中丢失更新
Best forकिसके लिए最适合I/O-boundI/O-boundI/O 密集CPU-boundCPU-boundCPU 密集
🔧
Fixed: Start-Up and Memory Numbersसुधार: Start-Up और Memory के Numbers已修正:启动时间和内存数字

The original said "~100 ms" to start a process and "~8 MB per thread / ~30 MB per process". We measured process start-up on our machine (Start-Method Lab, Section 05): about 1.5 ms with fork and 41 ms with spawn. The 8 MB per thread is only a reserved stack address range on Linux, not memory actually used. A fresh Python process typically uses around 10 MB or more, depending on what it imports.

मूल में process शुरू करने के लिए "~100 ms" और "~8 MB प्रति thread / ~30 MB प्रति process" कहा गया था। हमने अपनी machine पर process start-up मापा (Start-Method Lab, Section 05): fork से लगभग 1.5 ms और spawn से 41 ms। प्रति thread 8 MB Linux पर सिर्फ़ reserved stack address range है, असल में इस्तेमाल हुई memory नहीं। एक नया Python process आम तौर पर लगभग 10 MB या ज़्यादा लेता है, यह इस पर निर्भर है कि वह क्या import करता है।

原文说启动一个进程需"约 100 毫秒",以及"每线程约 8 MB / 每进程约 30 MB"。我们在自己的机器上测量了进程启动时间(启动方式实验室,第 05 节):fork 约 1.5 毫秒,spawn 约 41 毫秒。每线程 8 MB 只是 Linux 上预留的栈地址范围,并不是实际占用的内存。一个新的 Python 进程通常占用约 10 MB 或更多,取决于它导入了什么。


SectionSection第 03 节

Your First Process — The Basics 🚀आपका पहला Process — बुनियाद 🚀你的第一个进程 — 基础 🚀

multiprocessing.Process looks almost exactly like threading.Thread. If you know threads, you already know most of processes: target, args, start(), join().

multiprocessing.Process लगभग बिल्कुल threading.Thread जैसा दिखता है। अगर आप threads जानते हैं, तो processes का ज़्यादातर हिस्सा पहले से जानते हैं: target, args, start(), join()।

multiprocessing.Process 和 threading.Thread 几乎一模一样。如果你会用线程,你就已经掌握了进程的大部分:target、args、start()、join()。

from multiprocessing import Process
import os
import time

def worker(name, duration):
    print(f"[{name}] pid={os.getpid()} starting")
    time.sleep(duration)
    print(f"[{name}] pid={os.getpid()} done")

if __name__ == "__main__":                # REQUIRED on Windows and macOS
    print(f"Main pid={os.getpid()}")
    processes = []
    for i in range(3):
        p = Process(target=worker, args=(f"P{i}", 2))
        p.start()
        processes.append(p)
    for p in processes:
        p.join()
    print("All processes complete.")
OUTPUT
Main pid=1173 [P0] pid=1174 starting [P1] pid=1175 starting [P2] pid=1176 starting [P0] pid=1174 done [P1] pid=1175 done [P2] pid=1176 done All processes complete.

Each child has its own process id (pid). All three slept for 2 seconds at the same time, so the whole program took about 2 seconds, not 6.

हर child का अपना process id (pid) है। तीनों ने एक साथ 2 seconds sleep किया, इसलिए पूरा program 6 नहीं, लगभग 2 seconds में चला।

每个子进程都有自己的进程 ID(pid)。三个进程同时 sleep 了 2 秒,所以整个程序大约用了 2 秒,而不是 6 秒。

⚠️
The if __name__ == "__main__" Ruleif __name__ == "__main__" नियमif __name__ == "__main__" 规则

With the spawn and forkserver start methods, each child starts a fresh Python and re-imports your script to find your functions. Code at the top level of the script runs again in every child. Put all process-starting code under if __name__ == "__main__": so it runs only in the parent.

spawn और forkserver start methods में हर child एक नया Python शुरू करता है और आपके functions ढूँढने के लिए आपकी script फिर से import करता है। Script के top level का code हर child में फिर चलता है। Process शुरू करने वाला सारा code if __name__ == "__main__": के नीचे रखिए ताकि वह सिर्फ़ parent में चले।

使用 spawn 和 forkserver 启动方式时,每个子进程都会启动一个全新的 Python,并 重新导入你的脚本 来找到你的函数。脚本顶层的代码会在每个子进程中再运行一遍。把所有启动进程的代码都放在 if __name__ == "__main__": 之下,让它只在父进程中运行。

What really happens without the guardGuard के बिना असल में क्या होता है没有保护时实际会发生什么

import multiprocessing as mp, os

ctx = mp.get_context("spawn")      # the start method Windows and macOS use

def worker():
    print("child is working")

print("top of script, pid", os.getpid())
p = ctx.Process(target=worker)     # NO  if __name__ == "__main__":  guard!
p.start()
p.join()
print("exit code of child:", p.exitcode)
OUTPUT
top of script, pid 1177 top of script, pid 1179 Traceback (most recent call last): ... File "multiprocessing/spawn.py", line 140, in _check_not_importing_main raise RuntimeError(''' ...<16 lines>... ''') RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase. (...) exit code of child: 1
🔧
Fixed: Not Infinite Recursion — a RuntimeError, and It Matters on Linux Tooसुधार: Infinite Recursion नहीं — एक RuntimeError, और Linux पर भी ज़रूरी已修正:不是无限递归 — 而是 RuntimeError,在 Linux 上也很重要

The original said each child "would try to start more children — an infinite recursion". Python stops that: the real run above shows the top-level print running twice (parent and child), then the child raises RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase, exits with code 1, and the worker never runs. Also, the guard is not only for Windows and macOS: since Python 3.14 the default start method on Linux is forkserver, which re-imports the script too.

मूल में कहा गया कि हर child "और children शुरू करने की कोशिश करेगा — infinite recursion"। Python इसे रोकता है: ऊपर का असली run top-level print को दो बार चलते दिखाता है (parent और child), फिर child RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase raise करता है, code 1 से exit होता है, और worker कभी नहीं चलता। साथ ही guard सिर्फ़ Windows और macOS के लिए नहीं: Python 3.14 से Linux पर default start method forkserver है, जो script को भी फिर से import करता है।

原文说每个子进程"会尝试启动更多子进程 — 无限递归"。Python 会阻止这种情况:上面的真实运行显示,顶层的 print 运行了 两次(父进程和子进程),然后子进程抛出 RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase,以退出码 1 结束,工作函数根本没有运行。另外,这个保护不只是为 Windows 和 macOS 准备的:从 Python 3.14 起,Linux 上的默认启动方式是 forkserver,它同样会重新导入脚本。


SectionSection第 04 节

Daemon Processes — Background Workers That End With the Parent 👻Daemon Processes — Parent के साथ ख़त्म होने वाले Background Workers 👻守护进程 — 随父进程结束的后台工作者 👻

The Office Cleaner After Hoursदफ़्तर का सफ़ाई कर्मचारी下班后的保洁员
Picture an office. During work hours the main staff (the parent process) does the important work. In the background a cleaner (a daemon process) empties bins and restocks paper. When the last staff member leaves and locks up, the cleaner is sent home too — nobody stays behind alone.

Daemon processes work like that: they run alongside the parent, and when the parent exits they are terminated automatically.
एक दफ़्तर सोचिए। काम के घंटों में मुख्य staff (parent process) ज़रूरी काम करता है। Background में एक सफ़ाई कर्मचारी (daemon process) कूड़ा ख़ाली करता है और काग़ज़ भरता है। जब आख़िरी staff सदस्य ताला लगाकर जाता है, सफ़ाई कर्मचारी को भी घर भेज दिया जाता है — कोई अकेला पीछे नहीं रहता।

Daemon processes ऐसे ही काम करते हैं: वे parent के साथ चलते हैं, और parent के exit होने पर अपने-आप बंद कर दिए जाते हैं।
想象一间办公室。工作时间里,主要员工(父进程)做重要的工作。后台有一位保洁员(守护进程)清空垃圾桶、补充纸张。当最后一位员工锁门离开时,保洁员也被送回家 — 没有人独自留下。

守护进程就是这样工作的:它们与父进程一起运行,父进程退出时,它们会被 自动终止。
Figure 2 — Daemon vs Non-Daemon Lifecycleचित्र 2 — Daemon बनाम Non-Daemon Lifecycle图 2 — 守护与非守护进程的生命周期
Daemon vs non-daemon process lifecycleNON-DAEMON CHILD — the parent waitsNON-DAEMON CHILD — parent इंतज़ार करता है非守护子进程 — 父进程会等待parentmain code → waits…childruns until it finishesexitDAEMON CHILD — terminated at exitDAEMON CHILD — exit पर बंद守护子进程 — 退出时被终止parentmain codedaemonrunning…terminate() — no cleanup ✗

Non-daemon children keep the parent waiting. Daemon children are terminated when the parent exits.

Non-daemon children parent को इंतज़ार कराते हैं। Daemon children parent के exit पर बंद कर दिए जाते हैं।

非守护子进程会让父进程等待。守护子进程在父进程退出时被终止。

Daemon in actionDaemon काम करते हुए守护进程实战

from multiprocessing import Process
import time

def background_monitor():
    while True:
        print("  [daemon] still watching...")
        time.sleep(1)

if __name__ == "__main__":
    p = Process(target=background_monitor)
    p.daemon = True                     # MUST be set before start()
    p.start()
    print("[main] doing work for 3 seconds...")
    time.sleep(3)
    print("[main] exiting - daemon will be killed automatically")
    # No p.join() needed - daemon dies with parent
OUTPUT
[main] doing work for 3 seconds... [daemon] still watching... [daemon] still watching... [daemon] still watching... [main] exiting - daemon will be killed automatically

The same program WITHOUT the daemon flagवही program daemon flag के बिना同一个程序,不设守护标志

from multiprocessing import Process
import time

def background_monitor():
    while True:
        print("  [child] still watching...")
        time.sleep(1)

if __name__ == "__main__":
    p = Process(target=background_monitor)
    # p.daemon = True   <-- omitted!
    p.start()
    time.sleep(3)
    print("[main] want to exit...")
    # PROGRAM HANGS FOREVER - the child loops, the parent waits for it
OUTPUT
[child] still watching... [child] still watching... [child] still watching... [main] want to exit... [child] still watching... [child] still watching... [we stopped the program after 5 s - it would hang for ever]
⚠️
Three Rules for Daemon ProcessesDaemon Processes के तीन नियम守护进程的三条规则

1. Set p.daemon = True (or Process(..., daemon=True)) before p.start(). Setting it after raises an error.
2. A daemon process cannot have children of its own — see the real run below.
3. Daemons are terminated abruptly (with terminate()): no finally blocks, no exit handlers. Never use them for work that needs a clean shutdown — open files, database transactions, half-written output.

1. p.daemon = True (या Process(..., daemon=True)) p.start() से पहले set कीजिए। बाद में set करने पर error आता है।
2. Daemon process के अपने children नहीं हो सकते — नीचे असली run देखिए।
3. Daemons अचानक बंद होते हैं (terminate() से): न finally blocks, न exit handlers। साफ़ shutdown चाहने वाले काम के लिए इन्हें कभी मत लीजिए — खुली files, database transactions, आधा लिखा output।

1. 在 p.start() 之前 设置 p.daemon = True(或 Process(..., daemon=True))。之后再设置会报错。
2. 守护进程 不能有自己的子进程 — 见下面的真实运行。
3. 守护进程会被 突然终止(用 terminate()):不运行 finally 块,也不运行退出处理函数。需要干净关闭的工作绝不要用它 — 打开的文件、数据库事务、写了一半的输出。

from multiprocessing import Process

def grandchild():
    print("grandchild running")

def daemon_worker():
    p = Process(target=grandchild)     # a daemon tries to start a child
    p.start()
    p.join()

if __name__ == "__main__":
    d = Process(target=daemon_worker, daemon=True)
    d.start()
    d.join()
    print("daemon exit code:", d.exitcode)
OUTPUT
Process Process-1: Traceback (most recent call last): ... File "multiprocessing/process.py", line 118, in start assert not _current_process._config.get('daemon'), \ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: daemonic processes are not allowed to have children daemon exit code: 1
🏹 Use a DAEMON forDAEMON किसके लिए适合用守护进程
Background logging / metrics collectorBackground logging / metrics collector后台日志 / 指标收集器
Heartbeat sender to a health-check serviceHealth-check service को heartbeat भेजना向健康检查服务发送心跳
Idle-connection watchdogIdle-connection watchdog空闲连接看门狗
Periodic cache refresherसमय-समय पर cache refresh定期刷新缓存
Any "if the parent dies, this is pointless" workकोई भी "parent मरे तो यह बेकार" वाला काम任何"父进程没了,这也就没意义"的工作
🏁 Use a NON-DAEMON forNON-DAEMON किसके लिए适合用非守护进程
Writing important data to disk or a databaseज़रूरी data disk या database में लिखना把重要数据写入磁盘或数据库
Uploading files to cloud storageCloud storage पर files upload करना上传文件到云存储
Rendering or encoding a videoVideo render या encode करना渲染或编码视频
An ML training epoch that must finishML training का epoch जो पूरा होना ही चाहिए必须完成的一轮机器学习训练
Anything with clean-up in a finally blockजिसमें finally block में सफ़ाई हो任何在 finally 块中有清理逻辑的工作
💡
A Graceful Alternative to DaemonsDaemons का सलीक़े वाला विकल्प比守护进程更优雅的做法

If you want background work and a clean shutdown, do not use daemon=True. Run a normal process and give it an Event: the parent calls event.set() when it exits, the child notices and leaves its loop. You get "stops with the parent" plus proper clean-up.

अगर आपको background काम और साफ़ shutdown दोनों चाहिए, तो daemon=True मत लीजिए। एक सामान्य process चलाइए और उसे एक Event दीजिए: parent exit होते समय event.set() बुलाता है, child इसे देखकर अपना loop छोड़ देता है। आपको "parent के साथ रुकना" और सही सफ़ाई दोनों मिलते हैं।

如果你既想要后台工作,又 想要干净的关闭,就不要用 daemon=True。运行一个普通进程并给它一个 Event:父进程退出时调用 event.set(),子进程察觉后跳出循环。这样既能"随父进程停止",又能正确清理。

from multiprocessing import Process, Event
import time
import atexit

def graceful_monitor(stop_event):
    try:
        while not stop_event.is_set():
            print("  [worker] doing work...")
            stop_event.wait(timeout=1)     # wakes early if signalled
    finally:
        print("  [worker] shutting down cleanly - flushing state")

if __name__ == "__main__":
    stop = Event()
    p = Process(target=graceful_monitor, args=(stop,))
    p.start()
    atexit.register(lambda: (stop.set(), p.join(timeout=5)))
    time.sleep(3)
    print("[main] done")
OUTPUT
[worker] doing work... [worker] doing work... [worker] doing work... [main] done [worker] shutting down cleanly - flushing state

Look at the last line: it printed after "[main] done". The atexit handler set the event when the parent finished, and the worker's finally block ran. A daemon would have been cut off instead.

आख़िरी line देखिए: यह "[main] done" के बाद print हुई। Parent के ख़त्म होने पर atexit handler ने event set किया, और worker का finally block चला। Daemon होता तो वह बीच में कट जाता।

看最后一行:它在"[main] done"之后 打印。父进程结束时 atexit 处理函数设置了事件,工作进程的 finally 块得以运行。如果是守护进程,它就会被直接切断。


SectionSection第 05 节

Fork vs Spawn — How Processes Are Created 🌱Fork बनाम Spawn — Processes कैसे बनते हैं 🌱Fork vs Spawn — 进程是如何创建的 🌱

Figure 3 — Fork vs Spawn Start Methodsचित्र 3 — Fork बनाम Spawn Start Methods图 3 — fork 与 spawn 启动方式
Fork vs spawn start methodsfork — copy the parentfork — parent की copyfork — 复制父进程spawn — start a fresh Pythonspawn — नया Python शुरू करेंspawn — 启动全新的 Pythonparentexact copymemory + globalsvery fast — no re-importबहुत तेज़ — re-import नहीं非常快 — 不重新导入unsafe with threads; not on Windowsthreads के साथ असुरक्षित; Windows पर नहीं与线程一起使用不安全;Windows 不支持parentnew pythonre-importsyour scriptslower start — clean stateधीमी शुरुआत — साफ़ state启动较慢 — 状态干净default on Windows and macOSWindows और macOS पर defaultWindows 和 macOS 上的默认方式forkserver: a clean server process forks the children (Linux default since 3.14)forkserver: एक साफ़ server process children fork करता है (3.14 से Linux default)forkserver:由一个干净的服务器进程来 fork 子进程(3.14 起为 Linux 默认)

Fork copies the running parent. Spawn starts a new interpreter that re-imports your script.

Fork चलते parent की copy बनाता है। Spawn एक नया interpreter शुरू करता है जो आपकी script फिर से import करता है।

fork 复制正在运行的父进程。spawn 启动一个新的解释器并重新导入你的脚本。

import multiprocessing as mp

# Set the start method explicitly for the same behaviour on every platform
if __name__ == "__main__":
    print("Default here:", mp.get_start_method(allow_none=True) or "(not chosen yet)")
    mp.set_start_method("spawn")     # or "fork", "forkserver" - call once only
    print("Available:   ", mp.get_all_start_methods())
    print("Current:     ", mp.get_start_method())
OUTPUT
Default here: (not chosen yet) Available: ['fork', 'spawn', 'forkserver'] Current: spawn
🔧
Fixed: Defaults Have Changed — and set_start_method() Works Only Onceसुधार: Defaults बदल चुके हैं — और set_start_method() सिर्फ़ एक बार चलता है已修正:默认值已改变 — 并且 set_start_method() 只能调用一次

The original called spawn "the modern default". Spawn is the default on Windows and on macOS (since 3.8). On Linux the default was fork up to 3.13 and is forkserver since Python 3.14. Also note: set_start_method() may be called only once per program — a second call raises RuntimeError: context has already been set. To use a different method in one place, use mp.get_context("spawn").

मूल ने spawn को "आधुनिक default" कहा। Spawn Windows पर और macOS (3.8 से) पर default है। Linux पर default 3.13 तक fork था और Python 3.14 से forkserver है। ध्यान दें: set_start_method() हर program में सिर्फ़ एक बार बुलाया जा सकता है — दूसरी call RuntimeError: context has already been set raise करती है। किसी एक जगह अलग method चाहिए तो mp.get_context("spawn") लीजिए।

原文称 spawn 是"现代默认方式"。spawn 是 Windows 和 macOS(自 3.8 起) 上的默认方式。在 Linux 上,3.13 及以前默认是 fork,从 Python 3.14 起是 forkserver。另外注意:set_start_method() 每个程序只能调用一次 — 第二次调用会抛出 RuntimeError: context has already been set。如果只想在某处使用不同的方式,请用 mp.get_context("spawn")。

🌱 Simulator: Start-Method Lab🌱 Simulator: Start-Method Lab🌱 模拟器:启动方式实验室

The same script was run with each start method. The parent changes a global CONFIG before starting a child. Does the child see the change? Is the module loaded again? How long does one start + join take? All real.

वही script हर start method से चलाई गई। Parent child शुरू करने से पहले एक global CONFIG बदलता है। क्या child को बदलाव दिखता है? क्या module फिर load होता है? एक start + join में कितना समय लगता है? सब असली।

同一个脚本分别用三种启动方式运行。父进程在启动子进程之前修改了全局变量 CONFIG。子进程能看到修改吗?模块会被再次加载吗?一次启动 + join 要多久?全部是真实数据。

Start-Method LabStart-Method Lab启动方式实验室3 real runs3 असली runs3 次真实运行
Start method:Start method:启动方式:
OutputOutput输出
start + join timestart + join समय启动 + join 用时
💡
Which One to Choose?कौन-सा चुनें?该选哪一个?

Use spawn (or forkserver) for portability and safety: the child starts clean, and your code will behave the same on every operating system. Use fork only on Linux when start-up speed really matters and your program has no other threads (forking a process that has threads can deadlock). Use forkserver when you start many workers from a clean state on Linux.

Portability और सुरक्षा के लिए spawn (या forkserver) लीजिए: child साफ़ शुरू होता है, और आपका code हर operating system पर एक जैसा चलेगा। fork सिर्फ़ Linux पर तब लीजिए जब start-up गति सच में मायने रखे और आपके program में कोई दूसरे threads न हों (threads वाले process को fork करने से deadlock हो सकता है)। Linux पर साफ़ state से कई workers शुरू करने हों तो forkserver लीजिए।

为了可移植性和安全,使用 spawn(或 forkserver):子进程从干净状态开始,代码在所有操作系统上表现一致。只有在 Linux 上真正在意启动速度、且程序没有其他线程时才用 fork(fork 一个带线程的进程可能导致死锁)。在 Linux 上需要从干净状态启动大量工作进程时,用 forkserver。


SectionSection第 06 节

Pool — The Workhorse of Parallel Python 🐎Pool — Parallel Python का मेहनती घोड़ा 🐎Pool — 并行 Python 的主力 🐎

Creating and joining processes by hand gets tedious. multiprocessing.Pool starts a fixed group of worker processes once and gives you high-level methods to spread work across them.

हाथ से processes बनाना और join करना थकाऊ हो जाता है। multiprocessing.Pool worker processes का एक तय समूह एक बार शुरू करता है और उनमें काम बाँटने के ऊँचे-स्तर के methods देता है।

手动创建和等待进程很快就会变得繁琐。multiprocessing.Pool 一次性启动一组固定的工作进程,并提供高级方法把工作分配给它们。

Figure 4 — How Pool Distributes Workचित्र 4 — Pool काम कैसे बाँटता है图 4 — Pool 如何分配工作
How Pool distributes workpool.map(fn, items)task queue(pickled)worker 0worker 1worker 2worker 3resultsin order (map)

Pool starts N workers once and feeds them tasks through an internal queue.

Pool N workers एक बार शुरू करता है और अंदर की queue से उन्हें tasks देता है।

Pool 一次性启动 N 个工作进程,并通过内部队列给它们分配任务。

from multiprocessing import Pool
import time

def heavy_compute(n):
    # Simulated CPU work
    total = 0
    for i in range(10_000_000):
        total += i * n
    return n, total

if __name__ == "__main__":
    numbers = list(range(1, 9))

    # --- Sequential ---
    start = time.perf_counter()
    results = [heavy_compute(n) for n in numbers]
    print(f"Sequential: {time.perf_counter() - start:.2f}s")

    # --- Pool with 4 workers ---
    start = time.perf_counter()
    with Pool(processes=4) as pool:
        results = pool.map(heavy_compute, numbers)
    print(f"Pool(4):    {time.perf_counter() - start:.2f}s")

    for n, total in results:
        print(f"  {n}: {total}")
OUTPUT (our 2-core machine)OUTPUT (हमारी 2-core machine)输出(我们的双核机器)
Sequential: 3.73s Pool(4): 1.87s 1: 49999995000000 2: 99999990000000 3: 149999985000000 4: 199999980000000 5: 249999975000000 6: 299999970000000 7: 349999965000000 8: 399999960000000
🔧
Fixed: The Speed-Up Is Limited by Your Coresसुधार: Speed-Up आपके Cores से सीमित है已修正:加速比受限于核心数

The original showed 6.42 s → 1.71 s ("~3.75× speedup") on an 8-core machine. On our 2-core machine the real result is 3.73 s → 1.87 s: about 2×. Asking for 4 workers on 2 cores does not give 4× — at most 2 processes can run at once. Expect a speed-up close to the number of cores you really have (minus some start-up and pickling cost), never more.

मूल ने 8-core machine पर 6.42 s → 1.71 s ("~3.75× speedup") दिखाया। हमारी 2-core machine पर असली नतीजा 3.73 s → 1.87 s है: लगभग 2×। 2 cores पर 4 workers माँगने से 4× नहीं मिलता — एक साथ ज़्यादा से ज़्यादा 2 processes चल सकते हैं। Speed-up आपके असली cores की संख्या के क़रीब (start-up और pickling की कुछ क़ीमत घटाकर) की उम्मीद रखिए, उससे ज़्यादा कभी नहीं।

原文在 8 核机器上展示了 6.42 秒 → 1.71 秒("约 3.75 倍加速")。在我们的双核机器上,真实结果是 3.73 秒 → 1.87 秒:约 2 倍。在 2 个核心上要求 4 个工作进程并不会得到 4 倍 — 同一时刻最多只有 2 个进程在运行。加速比只会接近你 实际拥有的核心数(再减去一些启动和序列化开销),绝不会更高。


SectionSection第 07 节

Pool Methods — Choosing the Right One 🧰Pool Methods — सही चुनना 🧰Pool 方法 — 选对那一个 🧰

MethodMethod方法Behaviourव्यवहार行为Returnsलौटाता है返回Use whenकब लें适用场景
map(fn, iter)Blocks, orderedरुकता है, क्रम में阻塞,有序List of results, in input orderInput क्रम में results की list按输入顺序的结果列表You need all results togetherसारे results एक साथ चाहिए需要一次拿到全部结果
map_async(fn, iter)Does not block, orderedनहीं रुकता, क्रम में不阻塞,有序AsyncResult (call .get())AsyncResult (.get() बुलाएँ)AsyncResult(调用 .get())You want to do other work meanwhileबीच में दूसरा काम करना है想同时做别的事
imap(fn, iter)Lazy iterator, orderedLazy iterator, क्रम में惰性迭代器,有序Iterator (streams results)Iterator (results बहते आते हैं)迭代器(流式返回结果)Big input; use results as they comeबड़ा input; results आते ही इस्तेमाल输入很大;结果一到就处理
imap_unordered(fn, iter)Lazy iterator, NOT orderedLazy iterator, क्रम में नहीं惰性迭代器,无序Iterator (fastest first)Iterator (सबसे तेज़ पहले)迭代器(先完成先返回)Order does not matter; best throughputक्रम ज़रूरी नहीं; सबसे अच्छा throughput不在乎顺序;吞吐量最高
apply(fn, args)Blocks the caller, one callCaller को रोकता है, एक call阻塞调用方,单次调用One resultएक result一个结果Rarely useful — you wait while one worker runs itकम ही उपयोगी — एक worker के चलाने तक आप रुकते हैं很少用 — 一个工作进程执行时你只能等
apply_async(fn, args)Does not block, one callनहीं रुकता, एक call不阻塞,单次调用AsyncResultSend off single tasksअकेले tasks भेजना单独提交任务
starmap(fn, iter_of_tuples)Blocks, ordered, unpacks tuplesरुकता है, क्रम में, tuples खोलता है阻塞,有序,解包元组List of resultsResults की list结果列表The function takes several argumentsFunction कई arguments लेता है函数接受多个参数

Practical examples of eachहर एक के व्यावहारिक उदाहरण每种方法的实际示例

from multiprocessing import Pool

def square(x): return x * x
def add(a, b): return a + b

def on_complete(results):
    print(f"Done! {len(results)} results")

if __name__ == "__main__":
    with Pool(4) as pool:
        # 1. map - simplest, blocks until done, results in order
        print(pool.map(square, [1, 2, 3, 4, 5]))

        # 2. starmap - for multi-argument functions
        print(pool.starmap(add, [(1, 2), (3, 4), (5, 6)]))

        # 3. imap_unordered - stream results as they finish
        for result in pool.imap_unordered(square, range(10)):
            print(result, end=" ")
        print()                       # finish the line

        # 4. apply_async - fire off a single task without blocking
        r = pool.apply_async(square, (10,))
        # ... do other work ...
        print(r.get(timeout=5))

        # 5. map_async with a callback
        r = pool.map_async(square, range(100), callback=on_complete)
        r.wait()
OUTPUT
[1, 4, 9, 16, 25] [3, 7, 11] 0 4 1 9 16 25 36 49 64 81 100 Done! 100 results
🔧
Fixed: Two Small Fixes in the Examples Codeसुधार: Examples Code में दो छोटे Fix已修正:示例代码中的两处小修正

In the original, print(result, end=" ") in the imap_unordered loop had no closing print(), so the next result (100) would be glued onto the same line. We added print(). We also moved on_complete to module level, next to the other functions, which keeps the example tidy. And the table said apply "blocks the pool": it blocks the caller — the other workers stay free.

मूल में imap_unordered loop के print(result, end=" ") के बाद कोई print() नहीं था, इसलिए अगला result (100) उसी line पर चिपक जाता। हमने print() जोड़ा। on_complete को बाक़ी functions के पास module level पर रखा, जिससे example साफ़ रहता है। और table में कहा गया था कि apply "pool को रोकता है": यह caller को रोकता है — बाक़ी workers ख़ाली रहते हैं।

原文 imap_unordered 循环中的 print(result, end=" ") 后面没有收尾的 print(),所以下一个结果(100)会粘在同一行。我们加上了 print()。我们还把 on_complete 移到了模块级,和其他函数放在一起,让示例更整洁。另外,表格说 apply"阻塞线程池":其实它阻塞的是 调用方 — 其他工作进程仍然空闲。

🐎 Simulator: Pool Lab (real traces)🐎 Simulator: Pool Lab (असली traces)🐎 模拟器:Pool 实验室(真实追踪)

8 CPU tasks of different sizes (task #0 is the biggest). Choose the number of workers, the method and the chunksize. The bars show which worker process ran which task; the row underneath shows when each result reached your code. 12 real runs.

अलग-अलग size के 8 CPU tasks (task #0 सबसे बड़ा)। Workers की संख्या, method और chunksize चुनिए। Bars दिखाती हैं कि कौन-सा worker process कौन-सा task चला; नीचे की row दिखाती है कि हर result आपके code तक कब पहुँचा। 12 असली runs।

8 个大小不同的 CPU 任务(任务 #0 最大)。选择工作进程数、方法和 chunksize。条形图显示哪个工作进程执行了哪个任务;下面一行显示 每个结果何时到达你的代码。12 次真实运行。

Pool LabPool LabPool 实验室12 real runs12 असली runs12 次真实运行
Workers:Workers:工作进程:
Method:Method:方法:
chunksize:
Results reach your codeResults आपके code तक पहुँचते हैं结果到达你的代码
💡
When Order Does Not Matter, Use imap_unorderedजब क्रम ज़रूरी न हो, imap_unordered लीजिए不在乎顺序时,用 imap_unordered

For long jobs (image processing, ML inference) use imap_unordered: results stream in the moment they finish, so you can show progress. For many tiny tasks, pass a chunksize (for example 10 or 100) so each worker gets a batch instead of one item per round trip. map() already picks a chunksize for you; imap() and imap_unordered() use 1 unless you say otherwise. In the Pool Lab, notice that chunksize=4 with 2 workers sends tasks #0–3 to one worker, so the big task #0 delays three small ones.

लंबे jobs (image processing, ML inference) के लिए imap_unordered लीजिए: results ख़त्म होते ही आते जाते हैं, इसलिए आप प्रगति दिखा सकते हैं। बहुत छोटे-छोटे कई tasks के लिए chunksize दीजिए (जैसे 10 या 100) ताकि हर worker को एक-एक item की जगह एक batch मिले। map() आपके लिए ख़ुद chunksize चुनता है; imap() और imap_unordered() 1 लेते हैं जब तक आप न बताएँ। Pool Lab में देखिए कि 2 workers के साथ chunksize=4 tasks #0–3 एक worker को देता है, इसलिए बड़ा task #0 तीन छोटे tasks को देर कराता है।

对于长时间任务(图像处理、机器学习推理),用 imap_unordered:结果一完成就流式返回,所以你可以显示进度。对于 大量很小的任务,传入 chunksize(例如 10 或 100),让每个工作进程一次拿一批,而不是每次往返只拿一个。map() 会自动为你选择 chunksize;imap() 和 imap_unordered() 默认是 1。在 Pool 实验室中注意:2 个工作进程、chunksize=4 时,任务 #0–3 都交给同一个工作进程,于是大任务 #0 拖慢了三个小任务。


SectionSection第 08 节

multiprocessing.Queue — Inter-Process Communication 📫multiprocessing.Queue — Processes के बीच बातचीत 📫multiprocessing.Queue — 进程间通信 📫

The Pneumatic Tube SystemPneumatic Tube System气动管道系统
Old banks had pneumatic tubes — capsules pushed between buildings by air pressure. Each department (process) was physically separate, but the tubes let them send capsules back and forth. You could not just hand someone a sheet of paper across the street — you had to pack it into a capsule and shoot it through the tube.

multiprocessing.Queue is the pneumatic tube of Python processes. Objects are pickled (turned into bytes), sent through an OS pipe, then unpickled on the other side. Slower than passing a reference between threads, but a safe way to move data between processes.
पुराने banks में pneumatic tubes होती थीं — हवा के दबाव से इमारतों के बीच धकेले जाने वाले capsules। हर विभाग (process) अलग था, पर tubes से वे capsules आगे-पीछे भेज सकते थे। आप सड़क पार किसी को सीधे काग़ज़ नहीं दे सकते थे — उसे capsule में बंद करके tube से भेजना पड़ता था।

multiprocessing.Queue Python processes की pneumatic tube है। Objects pickle होते हैं (bytes में बदलते हैं), OS pipe से भेजे जाते हैं, फिर दूसरी ओर unpickle होते हैं। Threads के बीच reference देने से धीमा, पर processes के बीच data भेजने का सुरक्षित तरीक़ा।
老式银行有气动管道 — 靠气压在楼宇之间推送的胶囊。每个部门(进程)在物理上是分开的,但管道让它们可以来回传送胶囊。你不能隔着马路直接递给别人一张纸 — 必须把它装进胶囊,再从管道里发射过去。

multiprocessing.Queue 就是 Python 进程之间的气动管道。对象被 pickle(转成字节),通过操作系统管道发送,然后在另一端反序列化。比在线程之间传递引用慢,但这是在进程之间传递数据的安全方式。
Figure 5 — A Queue Between Processesचित्र 5 — Processes के बीच एक Queue图 5 — 进程之间的队列
IPC queue between processesPROCESS APROCESS A进程 A{'id': 1}pickle→ bytespipeunpickle→ objectPROCESS BPROCESS B进程 B{'id': 1}B gets a COPY — changing it does not change A’s objectB को COPY मिलती है — उसे बदलने से A का object नहीं बदलताB 得到的是副本 — 修改它不会影响 A 的对象

multiprocessing.Queue pickles the object, sends the bytes through a pipe, and unpickles a copy on the other side.

multiprocessing.Queue object को pickle करता है, bytes pipe से भेजता है, और दूसरी ओर एक copy unpickle करता है।

multiprocessing.Queue 把对象 pickle,通过管道发送字节,再在另一端反序列化出一个副本。

Producer-consumer between processesProcesses के बीच producer-consumer进程之间的生产者-消费者

from multiprocessing import Process, Queue
import time
import os

def producer(q, count):
    for i in range(count):
        item = {"id": i, "pid": os.getpid(), "payload": i * 2}
        q.put(item)
        print(f"[P] produced {item}")
        time.sleep(0.1)
    q.put(None)                              # poison pill

def consumer(q):
    while True:
        item = q.get()
        if item is None: break
        print(f"    [C pid={os.getpid()}] got {item}")
        time.sleep(0.15)

if __name__ == "__main__":
    q = Queue(maxsize=10)
    p1 = Process(target=producer, args=(q, 5))
    p2 = Process(target=consumer, args=(q,))
    p1.start(); p2.start()
    p1.join();  p2.join()
    print("Done.")
OUTPUT
[P] produced {'id': 0, 'pid': 1207, 'payload': 0} [C pid=1208] got {'id': 0, 'pid': 1207, 'payload': 0} [P] produced {'id': 1, 'pid': 1207, 'payload': 2} [C pid=1208] got {'id': 1, 'pid': 1207, 'payload': 2} [P] produced {'id': 2, 'pid': 1207, 'payload': 4} [P] produced {'id': 3, 'pid': 1207, 'payload': 6} [C pid=1208] got {'id': 2, 'pid': 1207, 'payload': 4} [P] produced {'id': 4, 'pid': 1207, 'payload': 8} [C pid=1208] got {'id': 3, 'pid': 1207, 'payload': 6} [C pid=1208] got {'id': 4, 'pid': 1207, 'payload': 8} Done.
🔧
Fixed: A Pipe Plus a Feeder Thread, Not Shared Memoryसुधार: एक Pipe और Feeder Thread, Shared Memory नहीं已修正:是管道加馈送线程,而不是共享内存

The original said queue items travel "across an OS pipe or shared memory". multiprocessing.Queue uses a pipe: a hidden background "feeder" thread in the sending process pickles each item and writes it into the pipe. That feeder thread is also why put() returns before the data has really left — and why joining a process that still has unsent data can hang (Bug Doctor, Section 17).

मूल में कहा गया कि queue items "OS pipe या shared memory से" जाते हैं। multiprocessing.Queue एक pipe इस्तेमाल करता है: भेजने वाले process में एक छिपा background "feeder" thread हर item को pickle करके pipe में लिखता है। इसी feeder thread की वजह से put() data के सच में निकलने से पहले लौट आता है — और इसी वजह से ऐसे process को join करना जिसका data अभी नहीं गया, अटक सकता है (Bug Doctor, Section 17)।

原文说队列元素"通过操作系统管道或共享内存"传输。multiprocessing.Queue 使用的是管道:发送进程中有一个隐藏的后台"馈送"线程,负责把每个元素 pickle 后写入管道。正因为有这个馈送线程,put() 会在数据真正发出之前就返回 — 这也是为什么等待一个还有未发送数据的进程可能会卡住(Bug 医生,第 17 节)。

⚠️
Not Every Object Can Be Pickledहर Object Pickle नहीं हो सकता并不是所有对象都能被 pickle

Lambdas, functions defined inside other functions, open files, locks, generators, database connections and sockets cannot be pickled, so they cannot cross a process boundary. Stick to plain data (dicts, lists, numbers, strings), NumPy arrays and instances of module-level classes. Use module-level def functions, not lambdas. Real run:

Lambdas, दूसरे functions के अंदर बने functions, खुली files, locks, generators, database connections और sockets pickle नहीं हो सकते, इसलिए process की सीमा पार नहीं कर सकते। सादा data (dicts, lists, numbers, strings), NumPy arrays और module-level classes के instances लीजिए। Lambdas नहीं, module-level def functions लीजिए। असली run:

lambda、定义在其他函数内部的函数、打开的文件、锁、生成器、数据库连接和套接字都不能被 pickle,所以无法跨越进程边界。请使用普通数据(字典、列表、数字、字符串)、NumPy 数组和模块级类的实例。使用模块级的 def 函数,而不是 lambda。真实运行:

from multiprocessing import Pool

if __name__ == "__main__":
    with Pool(2) as pool:
        print(pool.map(lambda x: x * 2, [1, 2, 3]))   # a lambda cannot be pickled
OUTPUT
Traceback (most recent call last): ... File "multiprocessing/reduction.py", line 51, in dumps cls(buf, protocol).dump(obj) ~~~~~~~~~~~~~~~~~~~~~~~^^^^^ _pickle.PicklingError: Can't pickle <function <lambda> at 0x7f…>: attribute lookup <lambda> on __main__ failed

🥒 Simulator: Pickle Checker🥒 Simulator: Pickle Checker🥒 模拟器:Pickle 检查器

Can this object be sent to another process? Each answer is the real result of pickle.dumps() on Python 3.13.16.

क्या यह object दूसरे process को भेजा जा सकता है? हर जवाब Python 3.13.16 पर pickle.dumps() का असली नतीजा है।

这个对象能发送给另一个进程吗?每个答案都是在 Python 3.13.16 上运行 pickle.dumps() 的真实结果。

Pickle CheckerPickle CheckerPickle 检查器10 real checks10 असली जाँच10 次真实检查

SectionSection第 09 节

Pipes — The Two-Endpoint Alternative 🔌Pipes — दो सिरों वाला विकल्प 🔌管道 — 两端点的替代方案 🔌

multiprocessing.Pipe() returns two connected ends for two-way communication between exactly two processes. It is faster than a Queue for point-to-point traffic, but it is not safe for many producers or many consumers on the same end.

multiprocessing.Pipe() ठीक दो processes के बीच दो-तरफ़ा बातचीत के लिए दो जुड़े सिरे लौटाता है। सीधी बातचीत के लिए यह Queue से तेज़ है, पर एक ही सिरे पर कई producers या कई consumers के लिए सुरक्षित नहीं।

multiprocessing.Pipe() 返回两个相连的端点,用于 恰好两个 进程之间的双向通信。对于点对点通信,它比 Queue 快,但同一端有多个生产者或消费者时并不安全。

from multiprocessing import Process, Pipe

def worker(conn):
    conn.send({"status": "ready", "pid": 42})
    reply = conn.recv()                       # wait for the parent's message
    print(f"Worker got reply: {reply}")
    conn.close()

if __name__ == "__main__":
    parent_conn, child_conn = Pipe()
    p = Process(target=worker, args=(child_conn,))
    p.start()
    print("Parent got:", parent_conn.recv())
    parent_conn.send("go ahead")
    p.join()
OUTPUT
Parent got: {'status': 'ready', 'pid': 42} Worker got reply: go ahead
Propertyगुण特性QueuePipe
Endsसिरे端点Many producers, many consumersकई producers, कई consumers多个生产者,多个消费者Exactly 2 endsठीक 2 सिरे恰好 2 个端点
Speed (our 100 MB test)गति (हमारा 100 MB test)速度(我们的 100 MB 测试)Slower — extra feeder thread and locksधीमा — अतिरिक्त feeder thread और locks较慢 — 多了馈送线程和锁Faster — a raw OS pipeतेज़ — सीधा OS pipe较快 — 原始操作系统管道
Safe with several users on one endएक सिरे पर कई users के साथ सुरक्षित同一端多人使用是否安全Yesहाँ是No — add your own lockनहीं — अपना lock जोड़ें否 — 需自己加锁
APIput() / get()send() / recv()
Best forकिसके लिए最适合Work-queue patternsWork-queue patterns工作队列模式Parent-child two-way chatParent-child दो-तरफ़ा बातचीत父子进程双向通信

SectionSection第 10 节

Shared Memory — When Pickling Is Too Slow 💾Shared Memory — जब Pickling बहुत धीमा हो 💾共享内存 — 当 pickle 太慢时 💾

Pickling a big NumPy array to send it through a Queue is slow. For large data, Python offers real shared memory: one block of RAM that several processes read and write directly, with no copying.

बड़े NumPy array को Queue से भेजने के लिए pickle करना धीमा है। बड़े data के लिए Python असली shared memory देता है: RAM का एक block जिसे कई processes सीधे पढ़ते और लिखते हैं, बिना copy किए।

为了通过 Queue 发送一个大的 NumPy 数组而对它进行 pickle 是很慢的。对于大数据,Python 提供了 真正的共享内存:一块多个进程可以直接读写的内存,无需复制。

Figure 6 — Three Ways to Share Stateचित्र 6 — State बाँटने के तीन तरीक़े图 6 — 共享状态的三种方式
Three shared-memory strategiesValue / Arraysmall C numbersfast · with a lockManagerany Python objectflexible · slow (RPC)shared_memorybig raw buffersfastest · zero copy

Value/Array for small numbers, Manager for any Python object, shared_memory for big numeric data.

छोटे numbers के लिए Value/Array, किसी भी Python object के लिए Manager, बड़े numeric data के लिए shared_memory।

小数值用 Value/Array,任意 Python 对象用 Manager,大块数值数据用 shared_memory。

📦 Simulator: IPC Cost Lab📦 Simulator: IPC Cost Lab📦 模拟器:进程间通信开销实验室

How long does it take to hand a NumPy array to another process and get a tiny answer back? Measured on our machine (best of 2 runs). For shared_memory only the block's name travels; the child attaches and reads the data in place.

एक NumPy array दूसरे process को देकर छोटा-सा जवाब वापस पाने में कितना समय लगता है? हमारी machine पर मापा गया (2 runs में सबसे अच्छा)। shared_memory में सिर्फ़ block का नाम जाता है; child जुड़कर data वहीं पढ़ता है।

把一个 NumPy 数组交给另一个进程并拿回一个很小的答案需要多久?在我们的机器上测得(2 次中取最好)。使用 shared_memory 时只传递内存块的 名字;子进程连接后直接在原处读取数据。

IPC Cost LabIPC Cost Lab进程间通信开销实验室real measurementsअसली माप真实测量
Array size:Array size:数组大小:

SectionSection第 11 节

Shared Memory — Value and Array (Simple C Types) 🔢Shared Memory — Value और Array (सरल C Types) 🔢共享内存 — Value 与 Array(简单 C 类型)🔢

For a shared counter or a small fixed-size array of numbers, use Value and Array. They hold C-level numbers in shared memory and come with an optional lock.

साझा counter या numbers की छोटी, तय-size array के लिए Value और Array लीजिए। ये shared memory में C-level numbers रखते हैं और एक वैकल्पिक lock के साथ आते हैं।

对于共享计数器或固定大小的小型数值数组,使用 Value 和 Array。它们在共享内存中保存 C 级别的数值,并自带一个可选的锁。

from multiprocessing import Process, Value, Array, Lock

def increment(counter, lock, n):
    for _ in range(n):
        with lock:                              # atomic increment
            counter.value += 1

def square_in_place(arr, start, end):
    for i in range(start, end):
        arr[i] = arr[i] * arr[i]

if __name__ == "__main__":
    # --- Shared counter ---
    counter = Value('i', 0)                    # 'i' = signed int
    lock = Lock()
    procs = [Process(target=increment, args=(counter, lock, 10_000))
             for _ in range(4)]
    for p in procs: p.start()
    for p in procs: p.join()
    print(f"Final counter: {counter.value}")

    # --- Shared array ---
    arr = Array('d', range(20))               # 'd' = double
    p1 = Process(target=square_in_place, args=(arr, 0, 10))
    p2 = Process(target=square_in_place, args=(arr, 10, 20))
    p1.start(); p2.start()
    p1.join();  p2.join()
    print(list(arr[:]))
OUTPUT
Final counter: 40000 [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0, 144.0, 169.0, 196.0, 225.0, 256.0, 289.0, 324.0, 361.0]
🔑
Type Codes — and the Built-In LockType Codes — और अंदर का Lock类型码 — 以及内置锁

'i' = int, 'd' = double, 'f' = float, 'b' = signed char, 'B' = unsigned char, 'c' = char — the same codes as Python's array module. A Value already has its own lock: with counter.get_lock(): works without creating a separate Lock(). But counter.value += 1 on its own is still read-then-write, so it still needs that lock — see the Shared-State Lab below.

'i' = int, 'd' = double, 'f' = float, 'b' = signed char, 'B' = unsigned char, 'c' = char — Python के array module वाले ही codes। एक Value के पास पहले से अपना lock है: अलग Lock() बनाए बिना with counter.get_lock(): चलता है। पर अकेला counter.value += 1 अब भी पढ़ो-फिर-लिखो है, इसलिए उसे वह lock चाहिए — नीचे Shared-State Lab देखिए।

'i' = int,'d' = double,'f' = float,'b' = 有符号 char,'B' = 无符号 char,'c' = char — 与 Python 的 array 模块相同。Value 本身已经带有一把锁:不用另建 Lock(),直接写 with counter.get_lock(): 即可。但单独的 counter.value += 1 仍然是"先读再写",所以仍需要那把锁 — 见下面的共享状态实验室。

🔀 Simulator: Shared-State Lab🔀 Simulator: Shared-State Lab🔀 模拟器:共享状态实验室

4 processes each add 1 to a counter 5,000 times (expected total: 20,000). Four ways to hold the counter, each run 3 times for real. Compare the totals and the times.

4 processes हर एक counter में 5,000 बार 1 जोड़ते हैं (अपेक्षित कुल: 20,000)। Counter रखने के चार तरीक़े, हर एक 3 बार असल में चलाया गया। कुल और समय की तुलना कीजिए।

4 个进程各自给计数器加 1,共 5,000 次(预期总数:20,000)。计数器有四种保存方式,每种都真实运行 3 次。比较总数和用时。

Shared-State LabShared-State Lab共享状态实验室12 real runs12 असली runs12 次真实运行

SectionSection第 12 节

Shared Memory — Manager (Any Python Object) 🗂️Shared Memory — Manager (कोई भी Python Object) 🗂️共享内存 — Manager(任意 Python 对象)🗂️

To share dicts, lists or other Python objects, use a Manager. It starts a separate server process that owns the real object; every access from a worker is a small request to that server (a remote procedure call).

Dicts, lists या दूसरे Python objects बाँटने के लिए Manager लीजिए। यह एक अलग server process शुरू करता है जो असली object रखता है; worker की हर पहुँच उस server को एक छोटी request है (remote procedure call)।

要共享字典、列表或其他 Python 对象,使用 Manager。它会启动一个单独的 服务器进程 来持有真正的对象;工作进程的每次访问都是向该服务器发出的一个小请求(远程过程调用)。

from multiprocessing import Process, Manager

def worker(shared_dict, shared_list, worker_id):
    shared_dict[worker_id] = f"done by worker {worker_id}"
    shared_list.append(worker_id ** 2)

if __name__ == "__main__":
    with Manager() as manager:
        shared_dict = manager.dict()
        shared_list = manager.list()
        procs = [Process(target=worker, args=(shared_dict, shared_list, i))
                 for i in range(5)]
        for p in procs: p.start()
        for p in procs: p.join()
        print("Dict:", dict(shared_dict))
        print("List:", list(shared_list))
OUTPUT (order varies)OUTPUT (क्रम बदलता है)输出(顺序会变化)
Dict: {3: 'done by worker 3', 4: 'done by worker 4', 0: 'done by worker 0', 1: 'done by worker 1', 2: 'done by worker 2'} List: [9, 16, 0, 1, 4]
🔧
Fixed: The Order Is Not Guaranteedसुधार: क्रम की गारंटी नहीं已修正:顺序并不保证

The original showed the dict keys and the list in perfect order [0, 1, 4, 9, 16]. In our real run workers 3 and 4 finished first, so the dict starts with keys 3 and 4 and the list is [9, 16, 0, 1, 4]. The processes run in parallel, so they write in whatever order they finish. If you need order, sort afterwards or store results by key.

मूल ने dict keys और list को बिल्कुल क्रम में [0, 1, 4, 9, 16] दिखाया। हमारे असली run में workers 3 और 4 पहले ख़त्म हुए, इसलिए dict keys 3 और 4 से शुरू होती है और list [9, 16, 0, 1, 4] है। Processes parallel चलते हैं, इसलिए जिस क्रम में ख़त्म हों उसी क्रम में लिखते हैं। क्रम चाहिए तो बाद में sort कीजिए या results को key से रखिए।

原文显示字典的键和列表都是完美有序的 [0, 1, 4, 9, 16]。在我们的真实运行中,工作进程 3 和 4 最先完成,所以字典以键 3 和 4 开头,列表是 [9, 16, 0, 1, 4]。进程是并行运行的,按完成的先后写入。如果需要顺序,事后排序或按键存储结果。

🐌
Manager Is SlowManager धीमा हैManager 很慢

Every read or write to a Manager object is a round trip to the server process. In the Shared-State Lab, 20,000 locked increments took about 6.5 s with a Manager dict, but about 0.05 s with a Value — over 100× slower. Use a Manager for convenience at moderate access rates, not in hot loops.

Manager object को हर read या write server process तक आना-जाना है। Shared-State Lab में 20,000 locked increments Manager dict के साथ लगभग 6.5 s लेते थे, पर Value के साथ लगभग 0.05 s — 100× से ज़्यादा धीमा। Manager को सुविधा के लिए मध्यम दर पर लीजिए, तेज़ loops में नहीं।

对 Manager 对象的每次读写都要往返一次服务器进程。在共享状态实验室中,20,000 次加锁自增用 Manager 字典约需 6.5 秒,而用 Value 只需约 0.05 秒 — 慢了 100 多倍。Manager 适合在访问频率适中时图个方便,不要用在热点循环里。


SectionSection第 13 节

Shared Memory — The Modern shared_memory Module 🚄Shared Memory — आधुनिक shared_memory Module 🚄共享内存 — 现代的 shared_memory 模块 🚄

Added in Python 3.8, multiprocessing.shared_memory lets processes share a raw block of memory with no pickling. Together with NumPy, it is the fastest way to share large numeric arrays.

Python 3.8 में जुड़ा multiprocessing.shared_memory processes को बिना pickling के memory का एक कच्चा block बाँटने देता है। NumPy के साथ, बड़े numeric arrays बाँटने का यह सबसे तेज़ तरीक़ा है।

Python 3.8 新增的 multiprocessing.shared_memory 让进程无需 pickle 就能共享一块原始内存。配合 NumPy,它是共享大型数值数组最快的方式。

from multiprocessing import Process
from multiprocessing.shared_memory import SharedMemory
import numpy as np

def worker(shm_name, shape, dtype, start, end):
    # Attach to the existing shared memory block by name
    existing = SharedMemory(name=shm_name)
    arr = np.ndarray(shape, dtype=dtype, buffer=existing.buf)
    arr[start:end] *= 2                      # change in place - no copy, no pickle
    del arr                                  # drop the view before closing
    existing.close()

if __name__ == "__main__":
    # Allocate a 1-million-element float64 array in shared memory
    shape = (1_000_000,)
    dtype = np.float64
    nbytes = np.zeros(shape, dtype=dtype).nbytes
    shm = SharedMemory(create=True, size=nbytes)
    arr = np.ndarray(shape, dtype=dtype, buffer=shm.buf)
    arr[:] = np.arange(1_000_000)

    # Start 4 workers, each changing one slice
    chunk = 250_000
    procs = [Process(target=worker,
                     args=(shm.name, shape, dtype, i * chunk, (i + 1) * chunk))
             for i in range(4)]
    for p in procs: p.start()
    for p in procs: p.join()

    print("First 5 :", arr[:5])
    print("Last 5  :", arr[-5:])
    del arr
    shm.close()
    shm.unlink()                             # free the shared memory block
OUTPUT
First 5 : [0. 2. 4. 6. 8.] Last 5 : [1999990. 1999992. 1999994. 1999996. 1999998.]
🧹
Drop the NumPy View Before close()close() से पहले NumPy View छोड़िए在 close() 之前删除 NumPy 视图

We added del arr before close(). A NumPy array built on shm.buf still points into that memory; closing the block while the array exists can raise BufferError: cannot close exported pointers exist. Delete (or stop using) the array first, then close(), and call unlink() once, in the process that created the block.

हमने close() से पहले del arr जोड़ा। shm.buf पर बना NumPy array अब भी उस memory की ओर इशारा करता है; array रहते block बंद करने पर BufferError: cannot close exported pointers exist आ सकता है। पहले array हटाइए (या इस्तेमाल बंद कीजिए), फिर close(), और unlink() सिर्फ़ एक बार उस process में बुलाइए जिसने block बनाया।

我们在 close() 之前加了 del arr。基于 shm.buf 构建的 NumPy 数组仍然指向那块内存;数组还在时关闭内存块,可能抛出 BufferError: cannot close exported pointers exist。先删除(或停止使用)数组,再 close(),并且只在创建该内存块的进程中调用一次 unlink()。

ShareableList — a simpler alternativeShareableList — एक सरल विकल्पShareableList — 更简单的替代方案

Here is the original example, run for real:

यह मूल example है, असल में चलाया गया:

这是原始示例的真实运行结果:

from multiprocessing import Process
from multiprocessing.shared_memory import ShareableList

def worker(name):
    sl = ShareableList(name=name)
    sl[0] = "modified by child"
    sl[1] += 100
    sl.shm.close()

if __name__ == "__main__":
    sl = ShareableList(["hello", 42, 3.14, True])
    print("Before:", list(sl))
    p = Process(target=worker, args=(sl.shm.name,))
    p.start(); p.join()
    print("After :", list(sl))
    sl.shm.close(); sl.shm.unlink()
OUTPUT
Before: ['hello', 42, 3.14, True] Process Process-1: Traceback (most recent call last): ... File "multiprocessing/shared_memory.py", line 469, in __setitem__ raise ValueError("bytes/str item exceeds available storage") ValueError: bytes/str item exceeds available storage After : ['hello', 42, 3.14, True]
🔧
Fixed: ShareableList Items Cannot Growसुधार: ShareableList Items बढ़ नहीं सकते已修正:ShareableList 的元素不能变长

The original showed After : ['modified by child', 142, ...]. The real run fails: the child raises ValueError: bytes/str item exceeds available storage, and the list is unchanged. A ShareableList has a fixed size: each slot gets just enough bytes for its first value, and "hello" left no room for "modified by child". The fix is to reserve enough space when you create it:

मूल ने After : ['modified by child', 142, ...] दिखाया। असली run fail होता है: child ValueError: bytes/str item exceeds available storage raise करता है, और list नहीं बदलती। ShareableList का size तय होता है: हर slot को उसकी पहली value जितने ही bytes मिलते हैं, और "hello" ने "modified by child" के लिए जगह नहीं छोड़ी। इलाज है बनाते समय पर्याप्त जगह reserve करना:

原文显示 After : ['modified by child', 142, ...]。真实运行却失败了:子进程抛出 ValueError: bytes/str item exceeds available storage,列表没有任何变化。ShareableList 的 大小是固定的:每个槽位只分配了刚好装下第一个值的字节数,"hello" 没有给 "modified by child" 留下空间。修复方法是在创建时预留足够的空间:

from multiprocessing import Process
from multiprocessing.shared_memory import ShareableList

def worker(name):
    sl = ShareableList(name=name)
    sl[0] = "modified by child"        # fits: 32 characters were reserved
    sl[1] += 100
    sl.shm.close()

if __name__ == "__main__":
    sl = ShareableList([" " * 32, 42, 3.14, True])   # reserve room for the string
    sl[0] = "hello"
    print("Before:", list(sl))
    p = Process(target=worker, args=(sl.shm.name,))
    p.start(); p.join()
    print("After :", list(sl))
    sl.shm.close(); sl.shm.unlink()
OUTPUT
Before: ['hello', 42, 3.14, True] After : ['modified by child', 142, 3.14, True]
🔧
Fixed: The Speed Numbers, Measuredसुधार: Speed Numbers, मापे हुए已修正:实测的速度数字

The original said passing a 100 MB array through a Queue takes "~2 seconds" and through shared memory "~0.001 seconds". The IPC Cost Lab (Section 10) measured it on our machine: Queue about 0.41 s, Pipe about 0.32 s, shared_memory about 0.018 s — and that last number includes the child adding up all 100 MB. Still more than 20× faster, but the exact numbers depend on your machine.

मूल में कहा गया कि 100 MB array Queue से भेजने में "~2 seconds" और shared memory से "~0.001 seconds" लगते हैं। IPC Cost Lab (Section 10) ने इसे हमारी machine पर मापा: Queue लगभग 0.41 s, Pipe लगभग 0.32 s, shared_memory लगभग 0.018 s — और आख़िरी number में child का पूरे 100 MB को जोड़ना भी शामिल है। फिर भी 20× से ज़्यादा तेज़, पर सटीक numbers आपकी machine पर निर्भर हैं।

原文说通过 Queue 传递 100 MB 数组需"约 2 秒",通过共享内存需"约 0.001 秒"。进程间通信开销实验室(第 10 节)在我们的机器上测得:Queue 约 0.41 秒,Pipe 约 0.32 秒,shared_memory 约 0.018 秒 — 最后这个数字还包含了子进程对全部 100 MB 求和的时间。仍然快了 20 倍以上,但具体数字取决于你的机器。


SectionSection第 14 节

Synchronisation Primitives 🚦Synchronisation Primitives 🚦同步原语 🚦

Just like threads, processes sometimes need to coordinate. multiprocessing provides process-safe versions of the usual tools.

Threads की तरह processes को भी कभी-कभी तालमेल चाहिए। multiprocessing आम औज़ारों के process-safe versions देता है।

和线程一样,进程有时也需要协调。multiprocessing 提供了常用工具的进程安全版本。

🔒
Lock / RLock
one at a timeएक समय में एक一次一个
Only one process can hold it. Protects shared state such as a shared counter. Use with lock:.
सिर्फ़ एक process इसे रख सकता है। साझा counter जैसी साझा state बचाता है। with lock: लीजिए।
同一时刻只有一个进程能持有它。用来保护共享计数器之类的共享状态。使用 with lock:。
📣
Event
broadcast signalसबको signal广播信号
One process calls event.set(); every process waiting in event.wait() continues. Good for "start now" or "please stop".
एक process event.set() बुलाता है; event.wait() में रुका हर process आगे बढ़ता है। "अब शुरू" या "कृपया रुको" के लिए अच्छा।
一个进程调用 event.set(),所有在 event.wait() 中等待的进程都继续执行。适合"开始"或"请停止"信号。
🎟️
Semaphore
limited seatsसीमित सीटें有限名额
Lets at most N processes use a resource at once — for example, cap database connections or API calls across all workers.
एक साथ ज़्यादा से ज़्यादा N processes को किसी resource का इस्तेमाल करने देता है — जैसे सभी workers में database connections या API calls सीमित करना।
最多允许 N 个进程同时使用某个资源 — 例如限制所有工作进程的数据库连接数或 API 调用数。
🏁
Barrier
wait for allसबका इंतज़ार等待所有人
All N processes must reach barrier.wait() before any can continue. Good for phases: everyone finishes phase 1 before phase 2.
आगे बढ़ने से पहले सभी N processes को barrier.wait() तक पहुँचना होता है। Phases के लिए अच्छा: phase 2 से पहले सब phase 1 पूरा करें।
所有 N 个进程都到达 barrier.wait() 后,才能有任何一个继续。适合分阶段的工作:所有人完成第一阶段后再开始第二阶段。
📌
Condition
wait for a stateकिसी state का इंतज़ार等待某个状态
Wait until some condition becomes true; producers notify, consumers wait. More flexible than Event, but more complex.
किसी शर्त के सच होने तक इंतज़ार; producers सूचना देते हैं, consumers इंतज़ार करते हैं। Event से ज़्यादा लचीला, पर ज़्यादा जटिल।
等待某个条件成立;生产者发通知,消费者等待。比 Event 更灵活,但也更复杂。
👥
JoinableQueue
queue + task_donequeue + task_done队列 + task_done
Like Queue, plus task_done() and join() — the process version of queue.Queue for the poison-pill pattern.
Queue जैसा, साथ में task_done() और join() — poison-pill pattern के लिए queue.Queue का process version।
和 Queue 一样,另外支持 task_done() 和 join() — 相当于用于毒丸模式的 queue.Queue 的进程版本。
from multiprocessing import Process, Event, Barrier
import time
import random

def worker(worker_id, start_event, barrier):
    start_event.wait()                          # wait for the green light
    print(f"[W{worker_id}] phase 1 starting")
    time.sleep(random.uniform(0.5, 2))
    print(f"[W{worker_id}] phase 1 done, waiting at barrier")
    barrier.wait()                              # wait for all workers
    print(f"[W{worker_id}] phase 2 starting")

if __name__ == "__main__":
    N = 3
    start_event = Event()
    barrier     = Barrier(N)
    procs = [Process(target=worker, args=(i, start_event, barrier))
             for i in range(N)]
    for p in procs: p.start()
    time.sleep(1)                               # let workers set up
    print("All workers ready. GO!")
    start_event.set()                           # release all at once
    for p in procs: p.join()
OUTPUT
All workers ready. GO! [W1] phase 1 starting [W0] phase 1 starting [W2] phase 1 starting [W0] phase 1 done, waiting at barrier [W1] phase 1 done, waiting at barrier [W2] phase 1 done, waiting at barrier [W2] phase 2 starting [W0] phase 2 starting [W1] phase 2 starting

No worker started phase 1 before "GO!", and no worker started phase 2 until all three had finished phase 1 — even though their phase-1 times were random.

"GO!" से पहले किसी worker ने phase 1 शुरू नहीं किया, और तीनों के phase 1 पूरा करने तक किसी ने phase 2 शुरू नहीं किया — भले ही उनके phase-1 समय random थे।

在"GO!"之前没有任何工作进程开始第一阶段;在三个都完成第一阶段之前,也没有任何一个开始第二阶段 — 即使它们第一阶段的耗时是随机的。


SectionSection第 15 节

Real-World Example — Parallel Image Processing 🖼️असली उदाहरण — Parallel Image Processing 🖼️实战示例 — 并行图像处理 🖼️

A classic use of multiprocessing: apply a heavy filter to many images. We generated 24 noisy 1024×768 JPEG test images first.

Multiprocessing का एक classic इस्तेमाल: कई images पर भारी filter लगाना। हमने पहले 24 noisy 1024×768 JPEG test images बनाईं।

多进程的经典用法:对大量图片应用繁重的滤镜。我们先生成了 24 张 1024×768 的带噪点 JPEG 测试图片。

from multiprocessing import Pool, cpu_count
from PIL import Image, ImageFilter
from pathlib import Path
import time

INPUT_DIR  = Path("input_images")
OUTPUT_DIR = Path("output_images")
OUTPUT_DIR.mkdir(exist_ok=True)

def process_image(input_path):
    """Load, apply an expensive filter, save."""
    img = Image.open(input_path)
    img = img.filter(ImageFilter.GaussianBlur(radius=5))   # CPU-heavy filter chain
    img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
    img = img.convert("RGB")
    output_path = OUTPUT_DIR / input_path.name
    img.save(output_path, quality=85)
    return input_path.name

if __name__ == "__main__":
    images = sorted(INPUT_DIR.glob("*.jpg"))
    print(f"Processing {len(images)} images on {cpu_count()} cores")

    start = time.perf_counter()
    for path in images:                      # baseline: one after another
        process_image(path)
    print(f"Sequential: {time.perf_counter() - start:.2f}s")

    start = time.perf_counter()
    with Pool(processes=cpu_count()) as pool:
        # imap_unordered streams results, chunksize batches for efficiency
        for i, name in enumerate(pool.imap_unordered(process_image, images, chunksize=4), 1):
            print(f"  [{i}/{len(images)}] {name}")
    print(f"Pool({cpu_count()}):   {time.perf_counter() - start:.2f}s")
OUTPUT
Processing 24 images on 2 cores Sequential: 1.42s [1/24] photo00.jpg [2/24] photo01.jpg [3/24] photo02.jpg [4/24] photo03.jpg ... (18 more lines) [23/24] photo22.jpg [24/24] photo23.jpg Pool(2): 0.67s

Checking the claim: the same job with threadsदावे की जाँच: वही काम threads से验证说法:用线程做同样的工作

from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageFilter
from pathlib import Path
import time

INPUT_DIR, OUTPUT_DIR = Path("input_images"), Path("output_images")
OUTPUT_DIR.mkdir(exist_ok=True)

def process_image(input_path):                # the same function as before
    img = Image.open(input_path)
    img = img.filter(ImageFilter.GaussianBlur(radius=5))
    img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
    img.convert("RGB").save(OUTPUT_DIR / input_path.name, quality=85)

images = sorted(INPUT_DIR.glob("*.jpg"))
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as ex:     # THREADS, not processes
    list(ex.map(process_image, images))
print(f"2 threads:  {time.perf_counter() - start:.2f}s")
OUTPUT
2 threads: 0.73s
🔧
Fixed: Pillow Releases the GIL — Threads Were Almost as Fast Hereसुधार: Pillow GIL छोड़ता है — यहाँ Threads लगभग उतने ही तेज़ थे已修正:Pillow 会释放 GIL — 这里线程几乎一样快

The original said "threads would be crippled by the GIL" for this job. Our real runs say otherwise: 2 threads took 0.73 s, almost the same as the 2-process Pool (0.67 s), both about 2× faster than one after another (1.42 s). Pillow does its heavy filtering in C code that releases the GIL, so threads run it in parallel. Processes win when the heavy part is pure Python code. Always measure. (The original's "500 images, 7× on 8 cores" could not be checked here; on 2 cores the best possible is about 2×.)

मूल में कहा गया कि इस काम में "threads GIL से अपंग हो जाएँगे"। हमारे असली runs कुछ और कहते हैं: 2 threads ने 0.73 s लिए, लगभग 2-process Pool जितने (0.67 s), दोनों एक-के-बाद-एक (1.42 s) से लगभग 2× तेज़। Pillow अपना भारी filtering C code में करता है जो GIL छोड़ देता है, इसलिए threads उसे parallel चलाते हैं। Processes तब जीतते हैं जब भारी हिस्सा pure Python code हो। हमेशा मापिए। (मूल का "500 images, 8 cores पर 7×" यहाँ जाँचा नहीं जा सका; 2 cores पर ज़्यादा से ज़्यादा लगभग 2× संभव है।)

原文说在这项工作中"线程会被 GIL 拖垮"。我们的真实运行结果并非如此:2 个线程用时 0.73 秒,与 2 进程的 Pool(0.67 秒)几乎相同,两者都比依次执行(1.42 秒)快约 2 倍。Pillow 在 C 代码中完成繁重的滤镜计算,这些代码 会释放 GIL,所以线程能并行运行它们。只有当繁重部分是 纯 Python 代码时,进程才会胜出。一定要实测。(原文"500 张图片,8 核快 7 倍"在这里无法验证;在 2 核上最多约快 2 倍。)


SectionSection第 16 节

Real-World Example — Parallel Aggregation With Shared Memory 📊असली उदाहरण — Shared Memory के साथ Parallel Aggregation 📊实战示例 — 用共享内存做并行聚合 📊

Compute the mean of every row of a big NumPy array using all cores, without copying the data to each worker. The input lives in shared_memory; the output is a small shared Array. We added one line at the end: the same job as a single NumPy call.

एक बड़े NumPy array की हर row का mean सभी cores से निकालना, हर worker को data copy किए बिना। Input shared_memory में है; output एक छोटी साझा Array है। हमने आख़िर में एक line जोड़ी: वही काम एक अकेली NumPy call से।

用所有核心计算一个大型 NumPy 数组每一行的平均值,而不把数据复制给每个工作进程。输入放在 shared_memory 中;输出是一个小的共享 Array。我们在末尾加了一行:用一次 NumPy 调用完成同样的工作。

from multiprocessing import Process, Array, cpu_count
from multiprocessing.shared_memory import SharedMemory
import numpy as np
import time

def worker(shm_name, shape, dtype, out_arr, start_row, end_row):
    shm = SharedMemory(name=shm_name)
    data = np.ndarray(shape, dtype=dtype, buffer=shm.buf)
    for i in range(start_row, end_row):        # row means for our slice
        out_arr[i] = data[i].mean()
    del data
    shm.close()

if __name__ == "__main__":
    # A 20,000 x 1,000 dataset (~160 MB of float64)
    N_ROWS, N_COLS = 20_000, 1_000
    data = np.random.rand(N_ROWS, N_COLS)

    # Move it into shared memory
    shm = SharedMemory(create=True, size=data.nbytes)
    shared = np.ndarray(data.shape, dtype=data.dtype, buffer=shm.buf)
    shared[:] = data

    results = Array('d', N_ROWS)               # shared output - one mean per row

    n_workers = cpu_count()                    # split the rows across cores
    chunk = N_ROWS // n_workers
    start = time.perf_counter()
    procs = []
    for i in range(n_workers):
        s = i * chunk
        e = N_ROWS if i == n_workers - 1 else (i + 1) * chunk
        p = Process(target=worker,
                    args=(shm.name, data.shape, data.dtype, results, s, e))
        procs.append(p); p.start()
    for p in procs: p.join()
    print(f"Parallel:        {time.perf_counter() - start:.3f}s using {n_workers} workers")
    print(f"First 5 row means: {[round(x, 4) for x in results[:5]]}")

    start = time.perf_counter()                # baseline: ONE vectorised NumPy call
    means = data.mean(axis=1)
    print(f"NumPy, 1 process: {time.perf_counter() - start:.3f}s  same answer? {np.allclose(means, results[:])}")
    del shared
    shm.close(); shm.unlink()
OUTPUT
Parallel: 0.080s using 2 workers First 5 row means: [0.5073, 0.4863, 0.4943, 0.5048, 0.5084] NumPy, 1 process: 0.022s same answer? True
🔧
Fixed: A Single NumPy Call Beat the Parallel Versionसुधार: एक अकेली NumPy Call ने Parallel Version को हराया已修正:一次 NumPy 调用就胜过了并行版本

We reduced the data from 100,000 rows (~800 MB) to 20,000 rows (~160 MB) to fit our test machine. The parallel version took 0.080 s; one data.mean(axis=1) call in a single process took 0.022 s — about 3.6× faster, with the same answer. NumPy already runs this loop in fast C, while the workers loop row by row in Python and lock the shared Array on every write. Lesson: before splitting work across processes, check whether a vectorised NumPy call already solves it. Parallel shared memory pays off when each piece of work is heavy.

हमने data को 100,000 rows (~800 MB) से घटाकर 20,000 rows (~160 MB) किया ताकि यह हमारी test machine पर चले। Parallel version ने 0.080 s लिए; एक process में एक data.mean(axis=1) call ने 0.022 s — लगभग 3.6× तेज़, वही जवाब। NumPy यह loop पहले से तेज़ C में चलाता है, जबकि workers Python में row-दर-row loop करते हैं और हर write पर साझा Array को lock करते हैं। सबक़: processes में काम बाँटने से पहले जाँचिए कि कोई vectorised NumPy call पहले से इसे हल करती है या नहीं। Parallel shared memory तब फ़ायदा देती है जब काम का हर हिस्सा भारी हो।

为了适应我们的测试机器,我们把数据从 100,000 行(约 800 MB)缩小到 20,000 行(约 160 MB)。并行版本用时 0.080 秒;在单个进程中调用一次 data.mean(axis=1) 只用了 0.022 秒 — 快约 3.6 倍,答案相同。NumPy 已经用高速的 C 代码执行这个循环,而工作进程却在 Python 中逐行循环,并且每次写入都要锁住共享的 Array。教训:在把工作拆分到多个进程之前,先看看一次向量化的 NumPy 调用是否已经能解决问题。只有当每块工作都很繁重时,并行共享内存才划算。


SectionSection第 17 节

Common Pitfalls and Fixes 🩺आम ग़लतियाँ और उनके Fix 🩺常见陷阱与修复 🩺

⚠️ Multiprocessing bugs you will hitMultiprocessing bugs जो आपको मिलेंगे你一定会遇到的多进程 bug
Bug 1
Missing if __name__ == "__main__" → with spawn/forkserver every child re-runs your top-level code and fails with a RuntimeError. Always guard process creation.
if __name__ == "__main__" ग़ायब → spawn/forkserver में हर child आपका top-level code फिर चलाता है और RuntimeError से fail होता है। Process बनाने को हमेशा guard कीजिए।
缺少 if __name__ == "__main__" → 使用 spawn/forkserver 时,每个子进程都会重新运行你的顶层代码,并以 RuntimeError 失败。启动进程的代码一定要加保护。
Bug 2
Sending a lambda or a nested function → PicklingError. Move the function to module level.
Lambda या nested function भेजना → PicklingError। Function को module level पर ले जाइए।
发送 lambda 或嵌套函数 → PicklingError。把函数移到模块级。
Bug 3
Expecting a normal global to be shared → each process changes its own private copy. Return results, or use Manager, Value/Array or shared_memory.
साधारण global के बँटने की उम्मीद → हर process अपनी निजी copy बदलता है। Results लौटाइए, या Manager, Value/Array या shared_memory लीजिए।
以为普通全局变量会被共享 → 每个进程修改的都是自己的私有副本。请返回结果,或使用 Manager、Value/Array 或 shared_memory。
Bug 4
Not calling close() and unlink() on shared memory → the block can stay in /dev/shm. Python's resource tracker usually removes leftovers at exit with a warning, but after a crash they can stay until reboot.
Shared memory पर close() और unlink() न बुलाना → block /dev/shm में रह सकता है। Python का resource tracker आम तौर पर exit पर warning के साथ बचा हुआ हटा देता है, पर crash के बाद वे reboot तक रह सकते हैं।
没有对共享内存调用 close() 和 unlink() → 内存块可能留在 /dev/shm 中。Python 的资源跟踪器通常会在退出时清理残留并给出警告,但在崩溃后它们可能一直留到重启。
Bug 5
Passing huge objects to Pool.map → every task pays the pickling cost. Use shared_memory or send less data.
Pool.map को बड़े objects देना → हर task pickling की क़ीमत चुकाता है। shared_memory लीजिए या कम data भेजिए।
把巨大的对象传给 Pool.map → 每个任务都要付出序列化的代价。使用 shared_memory 或减少数据传输。
Bug 6
Zombie processes → a finished child that nobody joined stays as a "defunct" entry. Call p.join(), or use with Pool() as pool:.
Zombie processes → ख़त्म हुआ child जिसे किसी ने join नहीं किया, "defunct" entry बनकर रहता है। p.join() बुलाइए, या with Pool() as pool: लीजिए।
僵尸进程 → 已结束但无人 join 的子进程会作为"defunct"条目残留。调用 p.join(),或使用 with Pool() as pool:。
Bug 7
Errors in apply_async tasks vanish → the exception is only raised when you call .get(). Always call it, or pass an error_callback.
apply_async tasks के errors ग़ायब → exception तभी raise होता है जब आप .get() बुलाते हैं। हमेशा बुलाइए, या error_callback दीजिए।
apply_async 任务中的错误消失了 → 只有调用 .get() 时才会抛出异常。一定要调用它,或传入 error_callback。
Bug 8
Joining before draining a Queue → a child that put large data waits until it is read, so p.join() hangs — even with no maxsize. Read (get()) the results first, then join.
Queue ख़ाली करने से पहले join → बड़ा data डालने वाला child उसके पढ़े जाने तक रुकता है, इसलिए p.join() अटकता है — maxsize के बिना भी। पहले results पढ़िए (get()), फिर join कीजिए।
在取空队列之前就 join → 放入了大量数据的子进程会一直等到数据被读走,所以 p.join() 会卡住 — 即使没有设置 maxsize。先读取(get())结果,再 join。
🔧
Fixed: The Queue Deadlock Does Not Need a "Full" Queueसुधार: Queue Deadlock के लिए "भरी" Queue ज़रूरी नहीं已修正:队列死锁并不需要"满"队列

The original described Bug 8 as a child blocked on put() to a full Queue. The Bug Doctor below shows the real case with an unbounded Queue: the child put 10 MB, and after join(timeout=2) it was still alive. Its feeder thread must hand all the bytes to the pipe before the process can end, and the pipe only drains when someone reads. Python's docs call this "joining processes that use queues".

मूल ने Bug 8 को भरी Queue पर put() में अटका child बताया। नीचे Bug Doctor एक असीमित Queue के साथ असली case दिखाता है: child ने 10 MB डाले, और join(timeout=2) के बाद भी वह ज़िंदा था। Process ख़त्म होने से पहले उसके feeder thread को सारे bytes pipe को देने होते हैं, और pipe तभी ख़ाली होता है जब कोई पढ़ता है। Python के docs इसे "joining processes that use queues" कहते हैं।

原文把 Bug 8 描述为子进程在向一个 满 队列 put() 时阻塞。下面的 Bug 医生展示了一个使用 无界 队列的真实案例:子进程放入了 10 MB,join(timeout=2) 之后它仍然活着。它的馈送线程必须把所有字节交给管道后进程才能结束,而管道只有在有人读取时才会被清空。Python 文档称之为"joining processes that use queues"。

✅
Safe Pool Templateसुरक्षित Pool Template安全的 Pool 模板

Wrap your pool in with Pool() as pool:, catch errors inside each task and return them as data, and call .get() on async results so exceptions surface. This removes most multiprocessing bugs. Here is a runnable version (the original used undefined process and items):

अपने pool को with Pool() as pool: में रखिए, हर task के अंदर errors पकड़कर उन्हें data के रूप में लौटाइए, और async results पर .get() बुलाइए ताकि exceptions सामने आएँ। इससे ज़्यादातर multiprocessing bugs हट जाते हैं। यह चलने वाला version है (मूल में process और items define नहीं थे):

把线程池包在 with Pool() as pool: 中,在每个任务内部捕获错误并作为数据返回,并对异步结果调用 .get() 让异常暴露出来。这能消除大多数多进程 bug。下面是可直接运行的版本(原文中的 process 和 items 未定义):

from multiprocessing import Pool

def process(item):                       # your real work
    if item % 4 == 0:
        raise ValueError(f"bad item {item}")
    return item * 10

def safe_task(item):
    try:
        return {"ok": True, "result": process(item)}
    except Exception as e:
        return {"ok": False, "error": repr(e), "item": item}

if __name__ == "__main__":
    items = range(1, 11)
    with Pool(processes=2) as pool:
        results = pool.map(safe_task, items)
    ok   = [r for r in results if r["ok"]]
    fail = [r for r in results if not r["ok"]]
    print(f"{len(ok)} succeeded, {len(fail)} failed")
    print("failures:", [(r["item"], r["error"]) for r in fail])
OUTPUT
8 succeeded, 2 failed failures: [(4, "ValueError('bad item 4')"), (8, "ValueError('bad item 8')")]

🩺 Simulator: Bug Doctor🩺 Simulator: Bug Doctor🩺 模拟器:Bug 医生

Six real multiprocessing mistakes. Buggy code on the left, fixed code on the right, changed lines marked. Every output is from a real run.

छह असली multiprocessing ग़लतियाँ। बाईं ओर buggy code, दाईं ओर ठीक किया हुआ, बदली lines चिह्नित। हर output असली run से है।

六个真实的多进程错误。左边是有 bug 的代码,右边是修正后的代码,改动的行已标出。每个输出都来自真实运行。

Bug DoctorBug DoctorBug 医生6 bugs · real outputs6 bugs · असली outputs6 个 bug · 真实输出
❌ BuggyBuggy有 bug
✅ Fixedठीक किया已修正

SectionSection第 18 节

Choosing the Right Tool — Decision Table 🧭सही औज़ार चुनना — फ़ैसले की Table 🧭选择合适的工具 — 决策表 🧭

Needज़रूरत需求Useलीजिए使用Whyक्यों原因
Spread independent tasks over workersअलग-अलग tasks workers में बाँटना把独立任务分给工作进程Pool.map / imap_unorderedSimplest; everything built inसबसे सरल; सब कुछ अंदर最简单;一切都内置好了
Streaming producer-consumerलगातार producer-consumer流式生产者-消费者Queue + ProcessMany-to-many, safeकई-से-कई, सुरक्षित多对多,安全
Fast two-way parent-child channelतेज़ दो-तरफ़ा parent-child channel快速的父子双向通道PipeLess overhead than QueueQueue से कम overhead开销比 Queue 小
Shared counter or small numeric arrayसाझा counter या छोटी numeric array共享计数器或小型数值数组Value / ArrayBuilt-in lock, C speedअंदर lock, C की गति内置锁,C 的速度
Shared dict/list of Python objectsPython objects की साझा dict/list共享 Python 对象的字典/列表Manager().dict()Flexible, but slow (each access is a request)लचीला, पर धीमा (हर पहुँच एक request)灵活但慢(每次访问都是一次请求)
Large NumPy arrays or byte buffersबड़े NumPy arrays या byte buffers大型 NumPy 数组或字节缓冲区shared_memory + NumPyZero copy, very fastZero copy, बहुत तेज़零拷贝,非常快
Start/stop all workers togetherसभी workers को एक साथ शुरू/बंद करना同时启动/停止所有工作进程EventOne signal to everyoneसबको एक signal一个信号通知所有人
Move between phases togetherPhases में एक साथ आगे बढ़ना一起进入下一阶段BarrierAll must arrive before anyone goes onकिसी के बढ़ने से पहले सबको पहुँचना होगा所有人到齐后才继续
Limit access to a resourceकिसी resource तक पहुँच सीमित करना限制对某资源的访问SemaphoreCap concurrent DB or API callsएक साथ DB या API calls सीमित करें限制并发的数据库或 API 调用

SectionSection第 19 节

Golden Rules and Quiz 🏆सुनहरे नियम और Quiz 🏆黄金法则与小测验 🏆

🔑 Multiprocessing — Rules to RememberMultiprocessing — याद रखने वाले नियम多进程 — 必须记住的规则
1
Always put process creation under if __name__ == "__main__":. With spawn (Windows, macOS) and forkserver (Linux default since 3.14), children re-import your script — without the guard they fail with a RuntimeError.
Process बनाना हमेशा if __name__ == "__main__": के नीचे रखिए। Spawn (Windows, macOS) और forkserver (3.14 से Linux default) में children आपकी script फिर import करते हैं — guard के बिना वे RuntimeError से fail होते हैं।
启动进程的代码一定要放在 if __name__ == "__main__": 之下。使用 spawn(Windows、macOS)和 forkserver(3.14 起 Linux 默认)时,子进程会重新导入你的脚本 — 没有保护就会以 RuntimeError 失败。
2
Use multiprocessing for CPU-bound Python code, threads for I/O. If the heavy work happens in C code that releases the GIL (NumPy, Pillow), threads may be just as fast — measure.
CPU-bound Python code के लिए multiprocessing, I/O के लिए threads। अगर भारी काम GIL छोड़ने वाले C code में होता है (NumPy, Pillow), तो threads भी उतने ही तेज़ हो सकते हैं — मापिए।
CPU 密集的 Python 代码用多进程,I/O 用线程。如果繁重工作发生在会释放 GIL 的 C 代码中(NumPy、Pillow),线程可能同样快 — 一定要测量。
3
For batch work, start with Pool and imap_unordered: results stream in as they finish, with very little code.
Batch काम के लिए Pool और imap_unordered से शुरू कीजिए: results ख़त्म होते ही आते हैं, बहुत कम code में।
批处理工作先用 Pool 和 imap_unordered:结果一完成就流式返回,代码很少。
4
Set chunksize for many small tasks in imap/imap_unordered (their default is 1). map picks a chunksize for you. With a few large tasks, keep chunks small so one big task does not hold up others.
imap/imap_unordered में बहुत छोटे tasks के लिए chunksize set कीजिए (उनका default 1 है)। map आपके लिए chunksize चुनता है। कुछ बड़े tasks हों तो chunks छोटे रखिए ताकि एक बड़ा task दूसरों को न रोके।
在 imap/imap_unordered 中处理大量小任务时设置 chunksize(它们的默认值是 1)。map 会替你选择 chunksize。如果只有少量大任务,就让分块小一些,避免一个大任务拖住其他任务。
5
For large arrays, use shared_memory instead of pickling through a Queue. In our test, 100 MB took about 0.4 s through a Queue and about 0.02 s with shared memory.
बड़े arrays के लिए Queue से pickling की जगह shared_memory लीजिए। हमारे test में 100 MB Queue से लगभग 0.4 s और shared memory से लगभग 0.02 s में गया।
大型数组请使用 shared_memory,而不是通过 Queue 进行 pickle。在我们的测试中,100 MB 通过 Queue 约需 0.4 秒,用共享内存约 0.02 秒。
6
Always call close() on shared memory in every process, and unlink() once in the creator. Drop NumPy views before closing.
हर process में shared memory पर close() बुलाइए, और बनाने वाले में एक बार unlink()। बंद करने से पहले NumPy views हटाइए।
在每个进程中都对共享内存调用 close(),并在创建者中调用一次 unlink()。关闭前先删除 NumPy 视图。
7
Never expect a normal Python object to be shared between processes — each process has its own copy. Return results, or use Manager, Value/Array or shared_memory.
कभी उम्मीद मत कीजिए कि साधारण Python object processes के बीच बँटेगा — हर process की अपनी copy होती है। Results लौटाइए, या Manager, Value/Array या shared_memory लीजिए।
永远不要指望普通 Python 对象会在进程之间共享 — 每个进程都有自己的副本。请返回结果,或使用 Manager、Value/Array 或 shared_memory。
8
Only picklable objects can cross a process boundary. Lambdas, nested functions, open files, locks, generators and sockets cannot. Use module-level functions.
सिर्फ़ pickle होने वाले objects process की सीमा पार कर सकते हैं। Lambdas, nested functions, खुली files, locks, generators और sockets नहीं। Module-level functions लीजिए।
只有可 pickle 的对象才能跨越进程边界。lambda、嵌套函数、打开的文件、锁、生成器和套接字都不行。请使用模块级函数。
9
Drain a Queue before joining the process that fills it. A child with unsent data cannot finish, so join() would wait for ever.
Queue भरने वाले process को join करने से पहले Queue ख़ाली कीजिए। जिस child का data अभी नहीं गया, वह ख़त्म नहीं हो सकता, इसलिए join() हमेशा इंतज़ार करेगा।
在 join 填充队列的进程之前,先把队列取空。还有未发送数据的子进程无法结束,所以 join() 会永远等待。
10
Handle errors inside workers: catch them and return an error record. Otherwise Pool.map raises one exception and you lose the other results — and apply_async errors stay hidden until .get().
Workers के अंदर errors संभालिए: उन्हें पकड़कर error record लौटाइए। वरना Pool.map एक exception raise करता है और बाक़ी results खो जाते हैं — और apply_async के errors .get() तक छिपे रहते हैं।
在工作进程内部处理错误:捕获它们并返回错误记录。否则 Pool.map 会抛出一个异常,你会丢掉其他结果 — 而 apply_async 的错误在调用 .get() 之前一直隐藏着。
11
For CPU-bound pools, use about as many workers as you have cores (the Pool default). More workers only add switching cost.
CPU-bound pools के लिए लगभग उतने workers लीजिए जितने आपके cores हैं (Pool का default)। ज़्यादा workers सिर्फ़ switching की क़ीमत बढ़ाते हैं।
CPU 密集型的进程池,工作进程数大约等于核心数(Pool 的默认值)。更多的工作进程只会增加切换开销。
12
Set daemon=True before start(), only for background workers that should end with the parent. Daemons are terminated without clean-up and cannot start children of their own.
daemon=True start() से पहले set कीजिए, सिर्फ़ उन background workers के लिए जो parent के साथ ख़त्म होने चाहिए। Daemons बिना सफ़ाई के बंद होते हैं और अपने children शुरू नहीं कर सकते।
在 start() 之前设置 daemon=True,只用于应随父进程一起结束的后台工作者。守护进程被终止时不做清理,也不能启动自己的子进程。

Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题

🏆 QuizPick an answer to see why.जवाब चुनें और कारण देखें।选一个答案,看看原因。
🎯
Key Takeawayमुख्य सीख关键要点

Each process has its own interpreter, its own GIL and its own memory, so processes run Python code in true parallel — up to the number of cores you have. The price is that data must be pickled and copied between them, or placed in shared memory. Guard process creation with if __name__ == "__main__":, use Pool for batches, Queue or Pipe for messages, Value/Array, Manager or shared_memory for shared state, and always measure — sometimes threads or a single NumPy call are faster.

हर process का अपना interpreter, अपना GIL और अपनी memory होती है, इसलिए processes Python code सच में parallel चलाते हैं — आपके cores की संख्या तक। क़ीमत यह है कि data को उनके बीच pickle और copy करना पड़ता है, या shared memory में रखना पड़ता है। Process बनाने को if __name__ == "__main__": से guard कीजिए, batches के लिए Pool, संदेशों के लिए Queue या Pipe, साझा state के लिए Value/Array, Manager या shared_memory लीजिए, और हमेशा मापिए — कभी-कभी threads या एक अकेली NumPy call ज़्यादा तेज़ होती है।

每个进程都有自己的解释器、自己的 GIL 和自己的内存,所以进程能真正并行地运行 Python 代码 — 最多与你的核心数相同。代价是数据必须在进程之间 pickle 并复制,或者放进共享内存。用 if __name__ == "__main__": 保护进程创建,批处理用 Pool,传消息用 Queue 或 Pipe,共享状态用 Value/Array、Manager 或 shared_memory,并且一定要测量 — 有时线程或一次 NumPy 调用反而更快。

🐍 Try Python Yourself

Write, run and visualize Python step by step. No install needed.

You have completed Multiprocess. View all sections →