Ang `multiprocessing` na module sa Python
Ipinaliliwanag ng artikulong ito ang multiprocessing na module sa Python.
Inilalahad ng artikulong ito ang mga praktikal na tip para sa pagsusulat ng ligtas at episyenteng parallel processing code gamit ang multiprocessing module.
YouTube Video
Ang multiprocessing na module sa Python
Mga Batayan: Bakit gamitin ang multiprocessing?
Pinapayagan ng multiprocessing ang parallelization batay sa proseso, kaya maaari mong i-parallelize ang mga CPU-bound na gawain nang hindi nahahadlangan ng Python GIL (Global Interpreter Lock). Para sa I/O-bound na mga gawain, maaaring mas simple at mas angkop na gamitin ang threading o asyncio.
Simpleng paggamit ng Process
Una, narito ang isang pangunahing halimbawa ng pagpapatakbo ng function sa hiwalay na proseso gamit ang Process. Ipinapakita nito kung paano simulan ang proseso, maghintay hanggang matapos, at magpadala ng mga argumento.
1# Explanation:
2# This example starts a separate process to run `worker` which prints messages.
3# It demonstrates starting, joining, and passing arguments.
4
5from multiprocessing import Process
6import time
7
8def worker(name, delay):
9 # English comment in code per user's preference
10 for i in range(3):
11 print(f"Worker {name}: iteration {i}")
12 time.sleep(delay)
13
14if __name__ == "__main__":
15 p = Process(target=worker, args=("A", 0.5))
16 p.start()
17 print("Main: waiting for worker to finish")
18 p.join()
19 print("Main: worker finished")- Ipinapakita ng code na ito ang daloy kung saan inilulunsad ng pangunahing proseso ang child process na
workerat hinihintay ang pagtatapos nito gamit angjoin(). Maaaring magpasa ng mga argumento gamit angargs.
Simpleng parallelization gamit ang Pool (high-level API)
Ang Pool.map ay kapaki-pakinabang kapag gusto mong i-apply ang parehong function sa maraming independent na gawain. Awtomatikong pinamamahalaan nito ang mga worker process para sa iyo.
1# Explanation:
2# Use Pool.map to parallelize a CPU-bound function across available processes.
3# Good for "embarrassingly parallel" workloads.
4
5from multiprocessing import Pool, cpu_count
6import math
7import time
8
9def is_prime(n):
10 # Check primality (inefficient but CPU-heavy for demo)
11 if n < 2:
12 return False
13 for i in range(2, int(math.sqrt(n)) + 1):
14 if n % i == 0:
15 return False
16 return True
17
18if __name__ == "__main__":
19 nums = [10_000_000 + i for i in range(50)]
20 start = time.time()
21 with Pool(processes=cpu_count()) as pool:
22 results = pool.map(is_prime, nums)
23 end = time.time()
24 print(f"Found primes: {sum(results)} / {len(nums)} in {end-start:.2f}s")- Awtomatikong kinokontrol ng
Poolang bilang ng workers, at angmapay nagbabalik ng resulta sa orihinal na pagkakasunod-sunod.
Interprocess communication: Producer/Consumer pattern gamit ang Queue
Ang Queue ay isang First-In-First-Out (FIFO) queue na ligtas na naglilipat ng mga bagay sa pagitan ng mga proseso. Narito ang ilang mga karaniwang pattern.
1# Explanation:
2# Demonstrates a producer putting items into a Queue
3# and consumer reading them.
4# This is useful for task pipelines between processes.
5
6from multiprocessing import Process, Queue
7import time
8import random
9
10def producer(q, n):
11 for i in range(n):
12 item = f"item-{i}"
13 print("Producer: putting", item)
14 q.put(item)
15 time.sleep(random.random() * 0.5)
16 q.put(None) # sentinel to signal consumer to stop
17
18def consumer(q):
19 while True:
20 item = q.get()
21 if item is None:
22 break
23 print("Consumer: got", item)
24 time.sleep(0.2)
25
26if __name__ == "__main__":
27 q = Queue()
28 p = Process(target=producer, args=(q, 5))
29 c = Process(target=consumer, args=(q,))
30 p.start()
31 c.start()
32 p.join()
33 c.join()
34 print("Main: done")- Pinapayagan ng
Queuena ligtas mong maipasa ang data sa pagitan ng mga proseso. Karaniwan ang paggamit ng espesyal na halaga gaya ngNoneupang magpahiwatig ng pagtatapos.
Shared memory: Value at Array
Maaari mong gamitin ang Value at Array kapag kailangan mong magbahagi ng maliliit na numero o array sa pagitan ng mga proseso. Kailangan mong gumamit ng mga lock upang maiwasan ang mga alitan.
1# Explanation:
2# Use Value to share a single integer counter
3# and Array for a small numeric array.
4# Show how to use a Lock to avoid race conditions.
5
6from multiprocessing import Process, Value, Array, Lock
7import time
8
9def increment(counter, lock, times):
10 for _ in range(times):
11 with lock:
12 counter.value += 1
13
14def update_array(arr):
15 for i in range(len(arr)):
16 arr[i] = arr[i] + 1
17
18if __name__ == "__main__":
19 lock = Lock()
20 counter = Value('i', 0) # 'i' = signed int
21 shared_arr = Array('i', [0, 0, 0])
22
23 p1 = Process(target=increment, args=(counter, lock, 1000))
24 p2 = Process(target=increment, args=(counter, lock, 1000))
25 a = Process(target=update_array, args=(shared_arr,))
26
27 p1.start(); p2.start(); a.start()
28 p1.join(); p2.join(); a.join()
29
30 print("Counter:", counter.value)
31 print("Array:", list(shared_arr))- Ang
ValueatArrayay nagbabahagi ng data gamit ang mababang-level na mekanismo (shared memory sa C language level), hindi lamang Python mismo. Samakatuwid, ito ay angkop para sa mabilisang pagbasa at pagsulat ng kaunting datos, ngunit hindi ito angkop para sa paghawak ng malaking dami ng datos..
Advanced na pagbabahagi: Shared objects (dict, list) gamit ang Manager
Kung gusto mong gumamit ng mas flexible na shared object tulad ng list o dictionary, gamitin ang Manager().
1# Explanation:
2# Manager provides proxy objects like dict/list
3# that can be shared across processes.
4# Good for moderate-size shared state and easier programming model.
5
6from multiprocessing import Process, Manager
7import time
8
9def worker(shared_dict, key, value):
10 shared_dict[key] = value
11
12if __name__ == "__main__":
13 with Manager() as manager:
14 d = manager.dict()
15 processes = []
16 for i in range(5):
17 p = Process(target=worker, args=(d, f"k{i}", i*i))
18 p.start()
19 processes.append(p)
20 for p in processes:
21 p.join()
22 print("Shared dict:", dict(d))- Maginhawa ang
Managerpara sa pagbabahagi ng mga diksyunaryo at listahan, ngunit bawat access ay nagpapadala ng datos sa pagitan ng mga proseso at nangangailangan ngpickleconversion. Kaya, ang madalas na pag-update ng malaking halaga ng datos ay nagpapabagal ng pagpoproseso.
Synchronization mechanisms: Paano gamitin ang Lock at Semaphore
Gamitin ang Lock o Semaphore upang kontrolin ang sabayang access sa shared resources. Puwede mo itong gamitin nang mas maigsi gamit ang with statement.
1# Explanation:
2# Demonstrates using Lock to prevent simultaneous access to a critical section.
3# Locks are necessary when shared resources are not atomic.
4
5from multiprocessing import Process, Lock, Value
6
7def safe_add(counter, lock):
8 for _ in range(10000):
9 with lock:
10 counter.value += 1
11
12if __name__ == "__main__":
13 lock = Lock()
14 counter = Value('i', 0)
15 p1 = Process(target=safe_add, args=(counter, lock))
16 p2 = Process(target=safe_add, args=(counter, lock))
17 p1.start(); p2.start()
18 p1.join(); p2.join()
19 print("Counter:", counter.value)- Ang mga lock ay pumipigil sa data races, ngunit kung masyadong malaki ang naka-lock na bahagi, bababa ang performance ng parallel processing. Ang mga kinakailangang bahagi lamang ang dapat protektahan bilang critical section.
Pagkakaiba ng fork sa UNIX at ng pag-uugali sa Windows
Sa UNIX, awtomatikong dini-duplicate ang process gamit ang fork, kaya naging episyente ang memory sa copy-on-write. Sa Windows, sinisimulan ang mga proseso gamit ang spawn (na muling nag-iimport ng mga module), kaya dapat mong ingatan ang proteksyon ng entry point at global initialization.
1# Explanation: Check start method (fork/spawn) and set it if needed.
2# Useful for debugging platform-dependent behavior.
3
4from multiprocessing import get_start_method, set_start_method
5
6if __name__ == "__main__":
7 print("Start method:", get_start_method())
8
9 # uncomment to force spawn on Unix for testing
10 # set_start_method('spawn')- Maaaring tawagin ang
set_start_methodnang isang beses lamang sa simula ng iyong program. Mas ligtas na huwag itong baguhin nang basta-basta sa loob ng mga library.
Praktikal na halimbawa: Pagsusukat ng performance ng CPU-bound workloads (paghahambing)
Narito ang isang script na nagpapakita kung gaano kabilis ang pagpoproseso kapag ginamitan ng parallelization gamit ang multiprocessing. Dito, ginagamit natin ang Pool.
1# Explanation:
2# Compare sequential vs parallel execution times for CPU-bound task.
3# Helps understand speedup and overhead.
4
5import time
6from multiprocessing import Pool, cpu_count
7import math
8
9def heavy_task(n):
10 s = 0
11 for i in range(1, n):
12 s += math.sqrt(i)
13 return s
14
15def run_sequential(nums):
16 return [heavy_task(n) for n in nums]
17
18def run_parallel(nums):
19 with Pool(processes=cpu_count()) as p:
20 return p.map(heavy_task, nums)
21
22if __name__ == "__main__":
23 nums = [2000000] * 8 # heavy tasks
24 t0 = time.time()
25 run_sequential(nums)
26 seq = time.time() - t0
27 t1 = time.time()
28 run_parallel(nums)
29 par = time.time() - t1
30 print(f"Sequential: {seq:.2f}s, Parallel: {par:.2f}s")- Ipinakikita ng halimbawang ito na depende sa dami ng gawain at bilang ng proseso, maaari ring hindi maging epektibo ang parallelization dahil sa overhead. Mas malaki (“mas mabigat”) at mas independent ang gawain, mas malaki ang benepisyo.
Mahalagang mga pangunahing patakaran
Nasa ibaba ang mga pangunahing punto para sa ligtas at mahusay na paggamit ng multiprocessing.
- Sa Windows, muling ini-import ang mga module kapag nagsimula ang child process, kaya dapat mong protektahan ang entry point ng script gamit ang
if __name__ == "__main__":. - Ang komunikasyon sa pagitan ng mga proseso ay isinasagawa sa pamamagitan ng serialization (gamit ang
pickleconversion), kaya nagiging magastos ang paglipat ng malalaking object. - Dahil naglalagay ang
multiprocessingng mga proseso, karaniwan nang batay samultiprocessing.cpu_count()ang pagpapasya ng bilang ng proseso. - Nagiging masalimuot ang paggawa ng isa pang
Poolsa loob ng isang worker, kaya dapat iwasan hangga't maaari ang nesting ng mgaPoolinstances. - Dahil mahirap matukoy mula sa main process ang mga exception na nangyari sa child processes, kinakailangan na malinaw na magpatupad ng logging at error handling.
- I-set ang bilang ng proseso base sa CPU count at gumamit ng threads para sa I/O-bound na gawain.
Praktikal na payo sa disenyo
Narito ang ilang mahahalagang konsepto at pattern para sa pagdisenyo ng parallel processing.
- Episyente kung hahatiin ang mga proseso sa mga role gaya ng input reading (I/O), preprocessing (multi-CPU), at aggregation (serial) sa pamamagitan ng 'pipelining.'.
- Upang mapadali ang debugging, tiyaking gumagana muna ang operasyong nais mo sa iisang proseso bago i-parallelize.
- Para sa logging, paghiwa-hiwalayin ang mga log output kada proseso (hal. isama ang PID sa file name) para mas madaling tukuyin ang problema.
- Maghanda ng retry at timeout mechanism para ligtas ang pagbawi kapag nag-hang ang isang proseso.
Buod (Mahalagang punto na maaari mong magamit kaagad)
Malakas ang parallel processing, ngunit mahalagang tamang husgahan ang klase ng gawain, laki ng data, at gastos sa inter-process communication. Epektibo ang multiprocessing para sa CPU-bound na pagpoproseso, ngunit ang maling disenyo o synchroniszation error ay maaaring magpababa ng performance. Kung susundin mo ang mga pangunahing patakaran at pattern, makakagawa ka ng ligtas at episyenteng parallel na mga programa.
Maaari mong sundan ang artikulo sa itaas gamit ang Visual Studio Code sa aming YouTube channel. Paki-check din ang aming YouTube channel.