Python Multithreading onder de knie krijgen: GIL uitgelegd met praktische voorbeelden
Wat is een thread?
Een thread is een lichtgewicht uitvoeringseenheid binnen een enkel proces. Threads delen het geheugenruimte van het proces, wat efficiënte data-uitwisseling mogelijk maakt, terwijl het besturingssysteem ze plant om gelijktijdig op een of meer CPU-kernen te draaien.
Wat is een proces?
Een proces is een onafhankelijke programmainstantie. Wanneer je een applicatie start – zeg een browser of teksteditor – creëert het OS een apart proces met zijn eigen code, data en geheugenkaart.
Fundamenten van Python Multithreading
Python's threading-module biedt een high-level API voor het beheren van threads, terwijl de verouderde _thread-module afgeschreven is en alleen bewaard blijft voor achterwaartse compatibiliteit. Threads zijn ideaal voor I/O-gebonden taken, terwijl CPU-gebonden werk de multiprocessing-module moet gebruiken om de GIL te omzeilen.
- Thread: gedeeld geheugen, lichtgewicht.
- Proces: geïsoleerd geheugen, zwaarder.
- GIL: een mutex die Python-bytecode-uitvoering beschermt.
Wanneer multithreading gebruiken
Multithreading schittert wanneer je verschillende I/O-zware operaties – netwerkverzoeken, bestands-I/O, of database-aanroepen – gelijktijdig moet uitvoeren. Goed ontworpen threads kunnen responsiviteit en doorvoer verbeteren zonder de overhead van het spawnen van nieuwe processen.
Belangrijke Python-modules voor threading
- _thread – Laag-niveau, afgeschreven.
- threading – Modern, rijk aan features, en de aanbevolen keuze.
De threading-module gebruiken
De threading-module biedt een Thread-klasse, synchronisatieprimitieven (Lock, RLock, Semaphore, Condition, Event, Barrier) en hulpfuncties zoals activeCount() en enumerate(). Hieronder een minimaal voorbeeld dat thread-creatie en synchronisatie demonstreert.
import time
import _thread
def thread_test(name, wait):
for i in range(4):
time.sleep(wait)
print(f"Running {name}")
print(f"{name} has finished execution")
if __name__ == "__main__":
_thread.start_new_thread(thread_test, ("First Thread", 1))
_thread.start_new_thread(thread_test, ("Second Thread", 2))
_thread.start_new_thread(thread_test, ("Third Thread", 3))
Het uitvoeren van deze code produceert interleaved output die gelijktijdige uitvoering demonstreert.
Uitleg
- Importeer
timeen de legacy_thread-module. - Definieer
thread_testom werk uit te voeren en de voortgang te loggen. - Start drie onafhankelijke threads via
start_new_thread.
Modern threading met threading.Thread
In de praktijk zul je threading.Thread subclassen en de run()-methode overschrijven. Het volgende voorbeeld spiegelt het vorige gedrag maar gebruikt de high-level API.
import time
import threading
class ThreadTester(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name = name
self.delay = delay
def run(self):
for _ in range(4):
time.sleep(self.delay)
print(f"Running {self.name}")
print(f"{self.name} has finished execution")
if __name__ == "__main__":
threads = [
ThreadTester("First Thread", 1),
ThreadTester("Second Thread", 2),
ThreadTester("Third Thread", 3),
]
for t in threads:
t.start()
for t in threads:
t.join()
Uitleg
- Subclass
Threadom thread-gedrag te encapsuleren. - Overschrijf
__init__om thread-specifieke data op te slaan. - Overschrijf
runom de taak te definiëren. - Instantieer, start en join elke thread.
Veelvoorkomende threading-valkuilen
Deadlocks
Een deadlock treedt op wanneer twee of meer threads oneindig wachten op resources die door elkaar worden vastgehouden. Klassiek voorbeeld: het Dinende Filosofen-probleem – vijf filosofen concurreren om vijf vorken, elk heeft twee vorken nodig om te eten. Als elke filosoof eerst de rechter vork oppakt, ontstaat een circulaire wachtsituatie, waardoor alle voortgang stopt.
Race Conditions
Wanneer meerdere threads gedeelde data wijzigen zonder correcte coördinatie, kan de eindtoestand onvoorspelbaar zijn. Bijvoorbeeld:
i = 0
for _ in range(100):
print(i)
i += 1
Het parallel starten van deze lus in threads kan een verwarde reeks produceren omdat increments overlappen.
Threads synchroniseren
De threading-module levert diverse synchronisatieprimitieven. Het meest fundamentele is Lock, dat exclusieve toegang tot een gedeelde resource garandeert.
import threading
lock = threading.Lock()
def worker(name):
for _ in range(5):
lock.acquire()
print(f"{name} acquired lock")
# kritieke sectie
lock.release()
if __name__ == "__main__":
t1 = threading.Thread(target=worker, args=("Thread A",))
t2 = threading.Thread(target=worker, args=("Thread B",))
t1.start(); t2.start()
t1.join(); t2.join()
De output demonstreert geserialiseerde toegang tot de kritieke sectie.
Andere synchronisatiehulpmiddelen
- RLock – re-entrant lock.
- Semaphore – regelt toegang tot een eindig aantal resources.
- Condition – laat threads wachten op specifieke voorwaarden.
- Event – simpele vlag voor inter-thread signalering.
- Barrier – blokkeert een set threads totdat alle een punt bereiken.
De Global Interpreter Lock (GIL) begrijpen
CPython gebruikt de GIL om bytecode-uitvoering over threads te serialiseren. Hoewel dit de interne structuren van de interpreter (bijv. reference counting) beschermt tegen race conditions, beperkt het ook parallelisme voor CPU-gebonden taken op multi-core systemen.
- Wanneer een thread start, acquirit het de GIL.
- Als een andere thread de GIL nodig heeft, blokkeert het tot de eerste het vrijgeeft.
- I/O-gebonden threads geven de GIL vrij tijdens blokkerende aanroepen, zodat anderen kunnen draaien.
- CPU-gebouden threads houden de GIL vast, wat andere threads verhindert Python-code uit te voeren.
Gevolg: multithreading in Python levert weinig voordeel op voor rekenzware workloads; multiprocessing is de voorkeursbenadering in die gevallen.
Waarom de GIL bestaat
De CPython garbage collector steunt op reference counting. Zonder een globale lock zouden gelijktijdige updates aan reference counts leiden tot race conditions en geheugencorruptie. Een per-object lock zou significante overhead introduceren, dus de GIL werd gekozen als een simpele, effectieve oplossing.
Praktische aanbevelingen
- Gebruik
threadingvoor I/O-gebonden gelijktijdigheid. - Kies
multiprocessingvoor CPU-gebonden parallelisme. - Synchroniseer gedeelde data altijd met locks of hogere primitieven.
- Wees je bewust van de GIL-impact bij schalen over kernen.
- Subclass
Threaden overschrijfrun()voor schone, onderhoudbare code.
Python
- Beheersing van C's free() functie: Praktische gids & voorbeeld
- C# Multithreading Beheersen: Prestaties en Efficiëntie Verhogen
- Python's strip()-methode beheersen: Uitgebreide handleiding & praktische voorbeelden
- Python's str.count() beheersen: Karakters & substrings tellen met voorbeelden
- Python round()-functie uitgelegd met praktische voorbeelden
- Python's map()-functie beheersen: syntaxis, voorbeelden en best practices
- Python timeit() – Uitvoeringstijd meten met praktische voorbeelden
- Python Counter in collections – Efficiënt tellen, updaten en rekenkundige operaties
- Python list.count(): Expert Gids met Praktische Voorbeelden
- Python List index() – Elementposities Vinden met Praktische Voorbeelden
- Python Multithreaded Programmering: Efficiënte Concurrency Technieken