`Multiprocessing`-modulen i Python

`Multiprocessing`-modulen i Python

Denne artikkelen forklarer multiprocessing-modulen i Python.

Denne artikkelen gir praktiske tips for å skrive trygg og effektiv kode for parallell prosessering ved bruk av multiprocessing-modulen.

YouTube Video

Multiprocessing-modulen i Python

Grunnleggende: Hvorfor bruke multiprocessing?

multiprocessing muliggjør parallellisering på prosessnivå, slik at du kan parallellisere CPU-bundne oppgaver uten å bli begrenset av Pythons GIL (Global Interpreter Lock). For I/O-bundne oppgaver kan threading eller asyncio være enklere og mer egnet.

Enkel bruk av Process

Her er først et grunnleggende eksempel på hvordan kjøre en funksjon i en egen prosess med Process. Dette viser hvordan du starter en prosess, venter på at den fullføres, og sender argumenter.

 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")
  • Denne koden viser flyten der hovedprosessen starter en barneprosess worker og venter på at den fullføres med join(). Du kan sende argumenter med args.

Enkel parallellisering med Pool (høynivå API)

Pool.map er nyttig når du vil bruke samme funksjon på flere uavhengige oppgaver. Den håndterer arbeidsprosesser internt for deg.

 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")
  • Pool kan automatisk styre antall arbeidere, og map returnerer resultater i opprinnelig rekkefølge.

Kommunikasjon mellom prosesser: Produsent/konsument-mønster med Queue

Queue er en først inn, først ut (FIFO) kø som på en sikker måte overfører objekter mellom prosesser. Nedenfor er noen typiske mønstre.

 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")
  • Queue lar deg trygt sende data mellom prosesser. Det er vanlig å bruke en spesiell verdi, som None, for å signalisere avslutning.

Delt minne: Value og Array

Du kan bruke Value og Array for å dele små tall eller tabeller mellom prosesser. Du må bruke låser for å unngå konflikter.

 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))
  • Value og Array deler data mellom prosesser ved hjelp av lavnivå-mekanismer (delt minne på C-språknivå), ikke i Python selv. Derfor er den egnet for rask lesing og skriving av små datamengder, men ikke egnet for håndtering av store datamengder..

Avansert deling: Delte objekter (dicts, lister) med Manager

Hvis du vil bruke mer fleksible delte objekter som lister eller ordbøker, bruk 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))
  • Manager er praktisk for å dele oppslagsverk og lister, men hver tilgang sender data mellom prosesser og krever pickle-konvertering. Derfor vil hyppig oppdatering av store datamengder bremse behandlingen.

Synkroniseringsmekanismer: Hvordan bruke Lock og Semaphore

Bruk Lock eller Semaphore for å styre samtidig tilgang til delte ressurser. Du kan bruke dem enkelt med with-setningen.

 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)
  • Låser forhindrer datakollisjoner, men hvis det låste området er for stort, vil ytelsen til parallell behandling reduseres. Bare de nødvendige delene bør beskyttes som en kritisk seksjon.

Forskjeller mellom fork på UNIX og oppførsel på Windows

På UNIX-systemer dupliseres prosesser som standard med fork, noe som gir effektiv minnehåndtering med copy-on-write. Windows starter prosesser med spawn (som re-importerer moduler), så du må være nøye med å beskytte inngangspunktet og global initialisering.

 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')
  • set_start_method kan bare kalles én gang ved programmets oppstart. Det er sikrere å ikke endre dette vilkårlig inne i biblioteker.

Praktisk eksempel: Benchmarking av CPU-bundne arbeidsoppgaver (sammenligning)

Nedenfor er et skript som enkelt sammenligner hvor mye raskere behandlingen kan være med parallellisering ved bruk av multiprocessing. Her bruker vi 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")
  • Dette eksemplet viser at avhengig av arbeidsmengde og antall prosesser, kan parallellisering noen ganger være ineffektiv grunnet overhead. Jo større (“tyngre”) og mer uavhengige oppgavene er, desto større er gevinsten.

Viktige grunnregler

Nedenfor er de grunnleggende punktene for å bruke multiprocessing trygt og effektivt.

  • På Windows re-importeres moduler når barneprosesser startes, så du må beskytte skriptets inngangspunkt med if __name__ == "__main__":.
  • Kommunikasjon mellom prosesser er serialisert (med pickle-konvertering), så overføring av store objekter blir kostbart.
  • Siden multiprocessing oppretter prosesser, er det vanlig å bestemme antall prosesser basert på multiprocessing.cpu_count().
  • Å opprette en ny Pool inne i en arbeider blir komplekst, så du bør unngå å nestle Pool-instanser så mye som mulig.
  • Siden unntak som oppstår i underprosesser er vanskelige å oppdage fra hovedprosessen, er det nødvendig å eksplisitt implementere logging og feilhåndtering.
  • Sett antall prosesser i henhold til CPU-en, og vurder å bruke tråder for I/O-bundne oppgaver.

Praktiske designtips

Nedenfor er noen nyttige konsepter og mønstre for å designe parallell behandling.

  • Det er effektivt å dele prosessene inn i roller som innlesning (I/O), forprosessering (multi-CPU) og aggregering (serielt) via 'pipelining'.
  • For å gjøre feilsøking enklere, test først funksjonaliteten i en enkel prosess før du parallelliserer.
  • For logging, skill loggutdata per prosess (f.eks. inkluder PID i filnavnene) for enklere feilisolering.
  • Forbered retry- og timeout-mekanismer slik at du trygt kan gjenopprette selv om en prosess henger.

Oppsummering (Nøkkelpunkter du kan ta i bruk med en gang)

Parallell prosessering er kraftfullt, men det er viktig å vurdere oppgavetype, datastørrelse og kommunikasjon mellom prosesser riktig. multiprocessing er effektivt for CPU-bundet prosessering, men dårlig design eller synkroniseringsfeil kan redusere ytelsen. Hvis du følger de grunnleggende reglene og mønstrene, kan du lage trygge og effektive parallelle programmer.

Du kan følge med på artikkelen ovenfor ved å bruke Visual Studio Code på vår YouTube-kanal. Vennligst sjekk ut YouTube-kanalen.

YouTube Video