Ga naar hoofdinhoud

Python-multiprocessing: een gids voor threads en processen

Leer hoe je threads en processen beheert met de multiprocessing-module van Python. Ontdek kerntechnieken voor parallel programmeren. Verbeter de efficiëntie van je code met voorbeelden.
· 7 min lezen

Verken met AI

De standaardbibliotheek van Python is uitgerust met verschillende ingebouwde pakketten waarmee ontwikkelaars direct de vruchten van de taal kunnen plukken.

Een van die pakketten is de module multiprocessing, die systemen in staat stelt meerdere processen tegelijk uit te voeren. Met andere woorden: ontwikkelaars kunnen toepassingen opdelen in kleinere threads die onafhankelijk van hun Python-code kunnen draaien. Deze threads of processen worden vervolgens door het besturingssysteem aan de processor toegewezen, zodat ze parallel kunnen draaien en de prestaties en efficiëntie van je Python-programma’s verbeteren. 

Als woorden als threads, processen, processors enz. je niet veel zeggen, geen zorgen. In dit artikel behandelen we wat een proces is, hoe processen zich onderscheiden van threads en hoe je de module multiprocessing gebruikt. 

Wat zijn processen in Python?

Het begrijpen van processen en threads is erg nuttig om beter te doorzien hoe een besturingssysteem programma’s beheert tijdens de verschillende uitvoeringsfasen. Een proces is simpelweg een verwijzing naar een computerprogramma. Elk programma heeft één proces dat eraan is gekoppeld. 

Terwijl je dit artikel leest, draait je computer waarschijnlijk al veel processen tegelijk, ook als je maar een paar programma’s open hebt – dat komt doordat de meeste besturingssystemen meerdere taken op de achtergrond uitvoeren. Je hebt bijvoorbeeld misschien maar drie programma’s open, maar je computer kan toch meer dan 30 actieve processen tegelijkertijd draaien. 

Hoe je de actieve processen op je computer bekijkt, hangt af van je besturingssysteem: 

  • Op Windows: Ctrl+Shift+Esc opent Taakbeheer 
  • Op Mac: open Spotlight en typ "Activity Monitor", druk dan op Return  
  • Op Linux: klik op Application Menu en zoek naar System Monitor. 

Python biedt toegang tot echte processen op systeemniveau. Door een instantie van de klasse Process uit de multiprocessing-module te maken, kunnen ontwikkelaars in Python verwijzen naar het onderliggende native proces. Wanneer een proces wordt gestart, wordt achter de schermen een nieuw native proces aangemaakt. De levenscyclus van een Python-proces bestaat uit drie fasen: het starten van een nieuw proces, het draaiende proces en het beëindigde proces – we behandelen elk van deze fasen.

Alle processen bestaan uit één of meer threads. We zeiden eerder dat “een proces simpelweg een verwijzing naar een computerprogramma is”. Elk Python-programma is een proces met één standaardthread: de main thread. De main thread is verantwoordelijk voor het uitvoeren van de instructies in je Python-programma’s. Het is echter belangrijk om te benadrukken dat processen en threads verschillend zijn. 

Multiprocessing vs. Threading

Concreter: één instantie van de Python-interpreter – de tool die door Python geschreven code vertaalt naar een taal die een computer begrijpt – staat gelijk aan één proces. Een proces bestaat uit ten minste één thread, in Python de “main thread” genoemd, al kunnen binnen hetzelfde proces extra threads worden gemaakt – alle andere threads die binnen een proces worden aangemaakt, behoren tot dat proces. 

De thread geeft weer hoe je Python-programma wordt uitgevoerd, en zodra alle niet-achtergrondthreads zijn beëindigd, stopt het Python-proces. 

  • Proces: Eén proces is een instantie van de Python-interpreter en bestaat uit minimaal één thread, de main thread. 
  • Thread: Een representatie van hoe een Python-programma binnen een Python-proces wordt uitgevoerd.

Python heeft twee erg vergelijkbare klassen die ons meer controle geven over processen en threads: multiprocessing.Process en threading.Thread.

Laten we hun overeenkomsten en verschillen doornemen. 

Overeenkomsten

Concurreny

Concurreny is een concept waarbij verschillende onderdelen van een programma buiten volgorde of gedeeltelijk in volgorde kunnen worden uitgevoerd, zonder dat het eindresultaat verandert. Beide klassen zijn oorspronkelijk bedoeld voor concurrency. 

Ondersteuning voor concurrency-primitieven

De klassen multiprocessing.Process en threading.Thread ondersteunen dezelfde concurrency-primitieven – hulpmiddelen die de synchronisatie en coördinatie van threads en processen mogelijk maken. 

Uniforme API

Als je de API van multiprocessing.Process onder de knie hebt, kun je die kennis overzetten naar de API van threading.Thread, en andersom. Ze zijn bewust zo ontworpen. 

Verschillen

Functionaliteit

Ondanks dat hun API’s hetzelfde zijn, verschillen processen en threads. Een proces is een hoger abstractieniveau dan een thread: een proces is een verwijzing naar een computerprogramma, en een thread behoort tot een proces. Dit verschil zit ingebakken in de klassen. De klassen representeren daardoor twee verschillende native functies die door het onderliggende besturingssysteem worden beheerd. 

Toegang tot gedeelde staat

De twee klassen gaan verschillend om met gedeelde staat. Omdat threads tot een proces behoren, kunnen ze binnen dat proces geheugen delen. Een functie die in een nieuwe thread wordt uitgevoerd, heeft dus nog steeds toegang tot dezelfde data en staat binnen een proces. De manier waarop threads staten delen staat bekend als “shared memory” en is vrij eenvoudig. Daarentegen is het delen van staat tussen processen veel ingewikkelder: de staat moet worden geserialiseerd en tussen processen verzonden. Met andere woorden: processen gebruiken geen shared memory om staten te delen, omdat ze afzonderlijk geheugen hebben. In plaats daarvan delen processen informatie via “inter-process communication”; om dat in Python te doen, heb je expliciete tools nodig zoals multiprocessing.Pipe of multiprocessing.Queue. 

GIL

De Python Global Interpreter Lock (GIL) is een lock die toestaat dat slechts één thread tegelijk controle heeft over de Python-interpreter. Meerdere threads vallen onder de GIL, wat multithreading in Python op de CPython-interpreter vaak ongunstig maakt: echte multicore-uitvoering via multithreading wordt niet ondersteund. Processen vallen echter niet onder de GIL, omdat de GIL binnen elk Python-proces wordt gebruikt en niet proces-overschrijdend. 

Qua use-cases is multiprocessing vaak beter dan threading in scenario’s waarin het programma CPU-intensief is en geen IO of gebruikersinteractie hoeft te doen. Threading is geschikter voor programma’s die IO- of netwerkgebonden zijn en in situaties waarin je de applicatie responsiever wilt maken. 

De voordelen van Python-multiprocessing

Denk aan een processor als een ondernemer. Naarmate het bedrijf van de ondernemer groeit, komen er meer taken bij die moeten worden beheerd om die groei bij te benen. Als de ondernemer besluit al deze taken alleen te doen (zoals boekhouding, sales, marketing, innovatie, enz.), dan dreigt de algehele efficiëntie en performance te lijden, want één persoon kan maar zoveel tegelijk doen. Voorbeeld: voordat ze met innovatietaken verder kan, moet ze stoppen met salestaken – dit heet taken “sequentieel” uitvoeren. 

De meeste ondernemers snappen dat alles alleen doen geen goed idee is. Daarom vangen ze de groeiende hoeveelheid werk doorgaans op door medewerkers aan te nemen voor verschillende afdelingen. Zo kunnen taken parallel worden uitgevoerd – de ene taak hoeft niet te stoppen voor een andere kan draaien. Meer medewerkers inhuren voor specifieke taken is vergelijkbaar met het gebruik van meerdere processors om bewerkingen uit te voeren. Zo zijn computer vision-projecten behoorlijk veeleisend omdat je vaak veel afbeeldingsdata moet verwerken, wat tijdrovend is: om dit te versnellen kun je meerdere afbeeldingen parallel verwerken. 

Daarom kunnen we stellen dat multiprocessing handig is om programma’s efficiënter te maken door taken te verdelen en toe te wijzen aan verschillende processors. De multiprocessing-module van Python maakt dit nog eenvoudiger door als high-level tool taken aan verschillende processen toe te wijzen en zo de efficiëntie van je programma’s te verhogen. 

De basis van Pythons multiprocessing-module 

We noemden dat de levenscyclus van een Python-proces uit drie fasen bestaat: het nieuwe proces, het draaiende proces en het beëindigde proces. In deze sectie gaan we dieper in op elke fase en geven we codevoorbeelden. Je kunt de code volgen in onze DataLab-werkmap. 

Het nieuwe proces 

Een nieuw proces is het proces dat is aangemaakt door een instantie van de klasse Process te initialiseren. Wanneer we het Process-object aan een variabele toewijzen, wordt een child-proces gespawned. 

from multiprocessing import Process

# Create a new process
process = Process()

Op dit moment doet onze procesinstantie niets, omdat we een leeg Process-object hebben geïnitialiseerd. We kunnen de configuratie van ons Process-object aanpassen om een specifieke functie uit te voeren door een functie die we in een ander proces willen draaien door te geven aan de target-parameter van de klasse. 

# Create a new process with a specified function to execute.
def example_function():
    pass

new_process = Process(target=example_function)

Als onze targetfunctie ook parameters had, geven we die simpelweg als tuple door aan de parameter args van het Process-object.

Tip: Leer functies schrijven in Python met de interactieve cursus Writing Functions in Python. 

# Create a new process with specific function to execute with args.
def example(args):
    pass

process = Process(target=example, args=("Hi",))

Let op: we hebben alleen een nieuw proces gecreëerd, maar het draait nog niet. 

Laten we kijken hoe we een nieuw proces kunnen starten.  

Het draaiende proces 

Een nieuw proces starten is vrij eenvoudig: roep gewoon de methode start() aan op de procesinstantie. 

# Run the new process
process.start()

 Deze actie start de activiteit van het proces door onder water de methode run() van de Process-instantie aan te roepen. De methode run() is ook verantwoordelijk voor het aanroepen van de aangepaste functie die is opgegeven in de target-parameter van de Process-instantie (als die is opgegeven).

Herinner je dat we eerder in de tutorial zeiden dat elk proces minstens één thread heeft, de main thread – dit is de standaard. Wanneer een child-proces wordt gestart, wordt de main thread voor dat child-proces gecreëerd en gestart. De main thread is verantwoordelijk voor het uitvoeren van al onze code in het child-proces. 

We kunnen controleren of onze procesinstantie leeft vanaf het moment dat de methode start() terugkeert totdat het child-proces beëindigt, door de methode is_alive() op onze Process-instantie aan te roepen. 

Let op: als je dit in een notebook test, moet de aanroep van is_alive() in dezelfde cel staan als de aanroep van de methode start() om het draaiende proces te kunnen vangen. 

# Run the new process
process.start()

# Check process is running
process.is_alive()

"""
True
"""

Als het proces niet draaide, zou de aanroep van de methode is_alive() False retourneren. 

Het beëindigde proces

Wanneer de functie run() terugkeert of afsluit, wordt het proces beëindigd: we hoeven niet per se expliciet iets te doen. Met andere woorden: je kunt verwachten dat een proces wordt beëindigd nadat de instructies van je targetfunctie zijn afgerond. De methode is_alive() zou dan false retourneren. 

# Check process is terminated - should return false.
process.is_alive()

"""
False
"""

Een proces kan echter ook worden beëindigd als het een niet-afgevangen uitzondering tegenkomt of als er een fout wordt opgegooid. Als er bijvoorbeeld een fout zit in de functie die je als target hebt ingesteld, wordt het proces beëindigd. 

# Create a new process with a specific function that has an error
# to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return name

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

# Check process is running
process.is_alive()

"""
True

Process Process-15:
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
"""

Het Process-object heeft ook de methoden terminate() en kill() waarmee je een proces geforceerd kunt beëindigen. 

# Create a new process with a specific function to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return split_args

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

if process.is_alive():
    process.terminate() # You can also use process.kill()
    print("Process terminated forcefully")

"""
Process terminated forcefully
"""

Belangrijk: geforceerde beëindigingen zijn niet de aanbevolen manier om een proces te stoppen; ze sluiten mogelijk niet alle open resources veilig of slaan de vereiste programmastatus niet op. Een betere oplossing is een gecontroleerde shutdown met een process-veilige booleaanse vlag of een vergelijkbaar hulpmiddel. 

Python-multiprocessing: tutorial

Nu je de basis van multiprocessing begrijpt, gaan we aan de slag met een voorbeeld om te laten zien hoe je concurrent programming in Python doet. 

De functie die we maken, print simpelweg een melding, slaapt 1 seconde en print dan nog een melding – leer meer over functies in deze tutorial over Python-functies. 

import time

def do_something():
    print("I'm going to sleep")
    time.sleep(1)
    print("I'm awake") 

De eerste stap is het aanmaken van een nieuw proces: we maken er twee. 

# Create new child process

process_1 = Process(target=do_something)
process_2 = Process(target=do_something)

Zo ziet een concurrent programma eruit in een notebook-omgeving: 

%%time

# Starts both processes
process_1.start()
process_2.start()

"""
I'm going to sleep
CPU times: user 810 µs, sys: 7.34 ms, total: 8.15 ms
Wall time: 6.04 ms
I'm going to sleep
"""

Afgaande op de uitvoer van het programma is het duidelijk dat er ergens een probleem in onze code zit. De timer wordt halverwege ons eerste proces geprint en de tweede print-instructie verschijnt niet. 

Dit komt doordat er drie processen draaien: het main-proces en process_1 en process_2. Het proces dat de tijd bijhoudt en print, is het main-proces. Wil je dat ons main-proces wacht met het printen van de tijd, dan moet je na het starten van de twee processen de methode join() aanroepen op beide.

Let op: als je hier meer over wilt weten, bekijk dan deze Stackoverflow-discussie. 

Hier is onze nieuwe code: 

%%time
​
# Create new child process (Cannot run a process more than once)
new_process_1 = Process(target=do_something)
new_process_2 = Process(target=do_something)
​
# Starts both processes
new_process_1.start()
new_process_2.start()
​
new_process_1.join()
new_process_2.join()

"""
I'm going to sleep
I'm going to sleep
I'm awake
I'm awake
CPU times: user 0 ns, sys: 14 ms, total: 14 ms
Wall time: 1.01 s
"""

Probleem opgelost. 

De wall time van deze run was iets langer dan de eerste run. Maar deze run rondde wel de aanroepen naar beide targetfuncties af voordat de tijdsinformatie werd teruggegeven. Met dezelfde redenering kun je ook meer processen tegelijkertijd laten draaien. 

Tot slot over Python-multiprocessing

In deze tutorial heb je geleerd hoe je Python-programma’s efficiënter maakt door ze gelijktijdig uit te voeren. Concreet heb je geleerd: 

  • Wat processen zijn en hoe je ze op je computer kunt bekijken.
  • De overeenkomsten en verschillen tussen Pythons multiprocessing- en threading-modules.
  • De basis van de multiprocessing-module en hoe je met multiprocessing een Python-programma gelijktijdig uitvoert. 

Meer leren over programmeren in Python? Bekijk ons Python Programmer-carrièrepad. Er is geen programmeerervaring nodig en aan het einde beschik je over de vaardigheden om software te ontwikkelen, data te bewerken en geavanceerde analyses in Python uit te voeren. 

Kurtis Pykes

Auteur

Kurtis Pykes

Onderwerpen

Gerelateerd