Industriële fabricage
Industrieel internet der dingen | Industriële materialen | Onderhoud en reparatie van apparatuur | Industriële programmering |
home  MfgRobots >> Industriële fabricage >  >> Industrial programming >> Python

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

  1. _thread – Laag-niveau, afgeschreven.
  2. 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

  1. Importeer time en de legacy _thread-module.
  2. Definieer thread_test om werk uit te voeren en de voortgang te loggen.
  3. 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

  1. Subclass Thread om thread-gedrag te encapsuleren.
  2. Overschrijf __init__ om thread-specifieke data op te slaan.
  3. Overschrijf run om de taak te definiëren.
  4. 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.

  1. Wanneer een thread start, acquirit het de GIL.
  2. Als een andere thread de GIL nodig heeft, blokkeert het tot de eerste het vrijgeeft.
  3. I/O-gebonden threads geven de GIL vrij tijdens blokkerende aanroepen, zodat anderen kunnen draaien.
  4. 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 threading voor I/O-gebonden gelijktijdigheid.
  • Kies multiprocessing voor CPU-gebonden parallelisme.
  • Synchroniseer gedeelde data altijd met locks of hogere primitieven.
  • Wees je bewust van de GIL-impact bij schalen over kernen.
  • Subclass Thread en overschrijf run() voor schone, onderhoudbare code.

Python

  1. Beheersing van C's free() functie: Praktische gids & voorbeeld
  2. C# Multithreading Beheersen: Prestaties en Efficiëntie Verhogen
  3. Python's strip()-methode beheersen: Uitgebreide handleiding & praktische voorbeelden
  4. Python's str.count() beheersen: Karakters & substrings tellen met voorbeelden
  5. Python round()-functie uitgelegd met praktische voorbeelden
  6. Python's map()-functie beheersen: syntaxis, voorbeelden en best practices
  7. Python timeit() – Uitvoeringstijd meten met praktische voorbeelden
  8. Python Counter in collections – Efficiënt tellen, updaten en rekenkundige operaties
  9. Python list.count(): Expert Gids met Praktische Voorbeelden
  10. Python List index() – Elementposities Vinden met Praktische Voorbeelden
  11. Python Multithreaded Programmering: Efficiënte Concurrency Technieken