De `threading` module in Python

De `threading` module in Python

Dit artikel legt de threading module in Python uit.

YouTube Video

De threading module in Python

De threading module in Python is een standaardbibliotheek die multithreading-programmering ondersteunt. Door threads te gebruiken kunnen meerdere processen gelijktijdig worden uitgevoerd, wat de prestaties van programma's kan verbeteren, vooral in gevallen van blokkerende bewerkingen zoals I/O-wachttijden. Vanwege de Global Interpreter Lock (GIL) in Python is de effectiviteit van multithreading beperkt voor CPU-intensieve operaties, maar het werkt efficiënt bij I/O-intensieve operaties.

De volgende secties leggen de basisprincipes van het gebruik van de threading module uit en hoe threads aangestuurd kunnen worden.

Basishandeling van threads

Threads maken en uitvoeren

Om een thread te maken en gelijktijdige verwerking uit te voeren, gebruik de klasse threading.Thread. Specificeer de doel-functie om een thread te maken en voer die thread uit.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10# Create and start the thread
11thread = threading.Thread(target=worker)
12thread.start()
13
14# Processing in the main thread
15print("Main thread continues to run")
16
17# Wait for the thread to finish
18thread.join()
19print("Main thread finished")
  • In dit voorbeeld wordt de worker-functie uitgevoerd in een aparte thread, terwijl de hoofdthread blijft werken. Door de methode join() aan te roepen, wacht de hoofdthread tot de subthread klaar is.

Threads een naam geven

Threads betekenisvolle namen geven maakt loggen en debuggen eenvoudiger. Je kunt dit opgeven met het argument name.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10t = threading.Thread(
11    target=worker,
12    args=("named-worker", 0.3),
13    name="MyWorkerThread"
14)
15
16t.start()
17
18print("Active threads:", threading.active_count())
19for th in threading.enumerate():
20    print(" -", th.name)
21
22t.join()
  • threading.enumerate() geeft een lijst van huidige threads terug, wat handig is voor debugging en het monitoren van de status.

De Thread-klasse erven

Als je de thread-uitvoerende klasse wilt aanpassen, kun je een nieuwe klasse definiëren door de klasse threading.Thread te erven.

 1import threading
 2import time
 3
 4# Inherit from the Thread class
 5class WorkerThread(threading.Thread):
 6    def __init__(self, name, delay, repeat=3):
 7        super().__init__(name=name)
 8        self.delay = delay
 9        self.repeat = repeat
10        self.results = []
11
12    def run(self):
13        for i in range(self.repeat):
14            msg = f"{self.name} step {i+1}"
15            print(msg)
16            self.results.append(msg)
17            time.sleep(self.delay)
18
19# Create and start the threads
20t1 = WorkerThread("Worker-A", delay=0.4, repeat=3)
21t2 = WorkerThread("Worker-B", delay=0.2, repeat=5)
22
23t1.start()
24t2.start()
25
26t1.join()
27t2.join()
28
29print("Results A:", t1.results)
30print("Results B:", t2.results)
  • In dit voorbeeld wordt de methode run() overschreven om het gedrag van de thread te definiëren, zodat elke thread zijn eigen gegevens kan behouden. Dit is handig wanneer threads complexe verwerking uitvoeren of als je wilt dat elke thread zijn eigen onafhankelijke gegevens heeft.

Synchronisatie tussen threads

Wanneer meerdere threads tegelijkertijd gedeelde bronnen benaderen, kunnen dataraces optreden. Om dit te voorkomen, biedt de threading module verschillende synchronisatiemechanismen.

Lock (Lock)

Het Lock-object wordt gebruikt om exclusieve controle over bronnen tussen threads te implementeren. Wanneer een thread een bron vergrendelt, kunnen andere threads die bron niet benaderen.

 1import threading
 2
 3lock = threading.Lock()
 4shared_resource = 0
 5
 6def worker():
 7    global shared_resource
 8    with lock:  # Acquire the lock
 9        local_copy = shared_resource
10        local_copy += 1
11        shared_resource = local_copy
12
13threads = [threading.Thread(target=worker) for _ in range(5)]
14
15for t in threads:
16    t.start()
17
18for t in threads:
19    t.join()
20
21print(f"Final value of shared resource: {shared_resource}")
  • In dit voorbeeld benaderen vijf threads een gedeelde bron, maar de Lock wordt gebruikt om te voorkomen dat meerdere threads de gegevens gelijktijdig wijzigen.

Opnieuw binnen te gaan slot (RLock)

Als dezelfde thread meerdere keren een slot moet verkrijgen, gebruik dan een RLock (herinvoerbaar slot). Dit is handig voor recursieve aanroepen of voor bibliotheekaanroepen die mogelijk sloten verwerven over verschillende aanroepen heen.

 1import threading
 2
 3rlock = threading.RLock()
 4shared = []
 5
 6def outer():
 7    with rlock:
 8        shared.append("outer")
 9        inner()
10
11def inner():
12    with rlock:
13        shared.append("inner")
14
15t = threading.Thread(target=outer)
16t.start()
17t.join()
18print(shared)
  • Met een RLock kan dezelfde thread een slot opnieuw verkrijgen dat hij al bezit, wat helpt om deadlocks bij geneste slotverwerving te voorkomen.

Voorwaarde (Condition)

Condition wordt gebruikt om threads te laten wachten tot een specifieke voorwaarde is vervuld. Wanneer een thread aan een voorwaarde voldoet, kun je notify() aanroepen om een andere thread te waarschuwen, of notify_all() om alle wachtende threads te waarschuwen.

Hieronder staat een voorbeeld van een producer en consument met een Condition.

 1import threading
 2
 3condition = threading.Condition()
 4shared_data = []
 5
 6def producer():
 7    with condition:
 8        shared_data.append(1)
 9        print("Produced an item")
10        condition.notify()  # Notify the consumer
11
12def consumer():
13    with condition:
14        condition.wait()  # Wait until the condition is met
15
16        item = shared_data.pop(0)
17        print(f"Consumed an item: {item}")
18
19# Create the threads
20producer_thread = threading.Thread(target=producer)
21consumer_thread = threading.Thread(target=consumer)
22
23consumer_thread.start()
24producer_thread.start()
25
26producer_thread.join()
27consumer_thread.join()
  • Deze code gebruikt een Condition zodat de producer een melding geeft wanneer er gegevens worden toegevoegd, en de consument wacht op die melding voordat hij de gegevens ophaalt, waardoor synchronisatie wordt bereikt.

Daemonisatie van threads

Daemon threads worden geforceerd beëindigd wanneer de hoofdthread eindigt. Terwijl normale threads moeten wachten om te beëindigen, worden daemon threads automatisch beëindigd.

 1import threading
 2import time
 3
 4def worker():
 5    while True:
 6        print("Working...")
 7        time.sleep(1)
 8
 9# Create a daemon thread
10thread = threading.Thread(target=worker)
11thread.daemon = True  # Set as a daemon thread
12
13thread.start()
14
15# Processing in the main thread
16time.sleep(3)
17print("Main thread finished")
  • In dit voorbeeld is de worker thread een daemon, dus wordt deze geforceerd beëindigd wanneer de hoofdthread eindigt.

Threadbeheer met ThreadPoolExecutor

Afgezien van de threading module, kun je de ThreadPoolExecutor uit de concurrent.futures module gebruiken om een threadpool te beheren en taken parallel uit te voeren.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def worker(seconds):
 5    print(f"Sleeping for {seconds} second(s)")
 6    time.sleep(seconds)
 7    return f"Finished sleeping for {seconds} second(s)"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(worker, i) for i in range(1, 4)]
11    for future in futures:
12        print(future.result())
  • ThreadPoolExecutor maakt een threadpool aan en verwerkt taken efficiënt. Geef het aantal gelijktijdig draaiende threads op met max_workers.

Eventcommunicatie tussen threads

Met threading.Event kun je vlaggen tussen threads instellen om andere threads op de hoogte te stellen van de gebeurtenissen.

 1import threading
 2import time
 3
 4event = threading.Event()
 5
 6def worker():
 7    print("Waiting for event to be set")
 8    event.wait()  # Wait until the event is set
 9
10    print("Event received, continuing work")
11
12thread = threading.Thread(target=worker)
13thread.start()
14
15time.sleep(2)
16print("Setting the event")
17event.set()  # Set the event and notify the thread
  • Deze code demonstreert een mechanisme waarbij de worker-thread wacht op het Event-signaal en doorgaat met verwerken wanneer de hoofdthread event.set() aanroept.

Foutafhandeling en beëindiging van threads

Wanneer er uitzonderingen optreden in threads, worden deze niet direct doorgegeven aan de hoofdthread, dus er is een patroon nodig om uitzonderingen op te vangen en te delen.

 1import threading
 2import queue
 3
 4def worker(err_q):
 5    try:
 6        raise ValueError("Something bad")
 7    except Exception as e:
 8        err_q.put(e)
 9
10q = queue.Queue()
11t = threading.Thread(target=worker, args=(q,))
12t.start()
13t.join()
14if not q.empty():
15    exc = q.get()
16    print("Worker raised:", exc)
  • Door uitzonderingen in een Queue te plaatsen en ze in de hoofdthread op te halen, kun je fouten betrouwbaar detecteren. Als je concurrent.futures.ThreadPoolExecutor gebruikt, worden uitzonderingen opnieuw opgegooid met future.result(), waardoor ze gemakkelijker af te handelen zijn.

De GIL (Global Interpreter Lock) en de gevolgen ervan

Door het mechanisme van de GIL (Global Interpreter Lock) in CPython, draaien meerdere Python-bytecodes niet daadwerkelijk gelijktijdig binnen hetzelfde proces. Voor taken die CPU-intensief zijn, zoals zware berekeningen, wordt aanbevolen om multiprocessing te gebruiken. Aan de andere kant werkt threading effectief voor I/O-gebonden taken zoals het lezen van bestanden of netwerkcommunicatie.

Samenvatting

Met de threading module in Python kun je multithreaded programma's implementeren en meerdere processen gelijktijdig uitvoeren. Met synchronisatiemechanismen zoals Lock en Condition kun je gedeelde bronnen veilig benaderen en complexe synchronisatie uitvoeren. Bovendien wordt threadbeheer en efficiënte parallelle verwerking eenvoudiger met daemon threads of ThreadPoolExecutor.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video