`threading`-modulet i Python

`threading`-modulet i Python

Denne artikel forklarer threading-modulet i Python.

YouTube Video

threading-modulet i Python

Threading-modulet i Python er et standardbibliotek, der understøtter multitrådet programmering. Brug af tråde gør det muligt for flere processer at køre samtidig, hvilket kan forbedre programmets ydeevne, især i situationer med blokerende operationer som I/O-ventetider. På grund af Pythons Global Interpreter Lock (GIL) er effektiviteten af multithreading begrænset ved CPU-tunge operationer, men det fungerer effektivt ved I/O-tunge operationer.

De følgende afsnit forklarer grundlaget for at bruge threading-modulet og hvordan man styrer tråde.

Grundlæggende brug af tråde

Oprettelse og kørsel af tråde

For at oprette en tråd og udføre samtidige processer skal du bruge threading.Thread-klassen. Angiv mål-funktionen for at oprette en tråd og udføre den tråd.

 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")
  • I dette eksempel udføres worker-funktionen i en separat tråd, mens hovedtråden fortsætter med at køre. Ved at kalde join()-metoden venter hovedtråden på, at undertråden bliver færdig.

Navngivning af tråde

At give tråde meningsfulde navne gør logning og fejlsøgning lettere. Du kan angive det med name-argumentet.

 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() returnerer en liste over aktuelle tråde, hvilket er nyttigt til fejlfinding og overvågning af tilstand.

Arv fra Thread-klassen

Hvis du vil tilpasse den trådeksekverende klasse, kan du definere en ny klasse ved at arve fra threading.Thread-klassen.

 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)
  • I dette eksempel overskrives run()-metoden for at definere trådens opførsel, hvilket gør det muligt for hver tråd at have sine egne data. Dette er nyttigt, når tråde udfører kompleks behandling, eller når du ønsker, at hver tråd skal have sine egne uafhængige data.

Synkronisering mellem tråde

Når flere tråde får adgang til delte ressourcer samtidig, kan der opstå data races. For at forhindre dette giver threading-modulet flere synkroniseringsmekanismer.

Lås (Lock)

Lock-objektet bruges til at implementere eksklusiv kontrol af ressourcer mellem tråde. Mens én tråd låser en ressource, kan andre tråde ikke få adgang til den ressource.

 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}")
  • I dette eksempel får fem tråde adgang til en delt ressource, men Lock bruges til at forhindre, at flere tråde ændrer dataene samtidig.

Reentrant lås (RLock)

Hvis den samme tråd skal erhverve en lås flere gange, skal du bruge en RLock (reentrant lås). Dette er nyttigt for rekursive kald eller for bibliotekskald, der kan erhverve låse på tværs af forskellige kald.

 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)
  • Med RLock kan den samme tråd genoptage en lås, den allerede holder, hvilket hjælper med at forhindre deadlocks ved indlejret låseoptagelse.

Betingelse (Condition)

Condition bruges til, at tråde kan vente, indtil en specifik betingelse er opfyldt. Når en tråd opfylder en betingelse, kan du kalde notify() for at give en anden tråd besked eller notify_all() for at give besked til alle ventende tråde.

Nedenfor er et eksempel på en producent og forbruger, der bruger en 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()
  • Denne kode bruger en Condition, så producenten giver besked, når data tilføjes, og forbrugeren venter på den besked, før den henter dataene, hvilket giver synkronisering.

Tråd-daemonisering

Daemon-tråde afsluttes med tvang, når hovedtråden slutter. Mens normale tråde skal vente på at afslutte, afsluttes daemon-tråde automatisk.

 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")
  • I dette eksempel er worker-tråden daemoniseret, så den afsluttes med tvang, når hovedtråden slutter.

Trådhåndtering med ThreadPoolExecutor

Udover threading-modulet kan du bruge ThreadPoolExecutor fra concurrent.futures-modulet til at administrere en pulje af tråde og udføre opgaver parallelt.

 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 opretter en trådpulje og behandler opgaver effektivt. Angiv antallet af tråde, der skal køre samtidig, med max_workers.

Hændelseskommunikation mellem tråde

Ved at bruge threading.Event kan du sætte flag mellem tråde for at informere andre tråde om, at en hændelse er sket.

 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
  • Denne kode demonstrerer en mekanisme, hvor arbejdståden venter på Event-signalet og genoptager behandlingen, når hovedtråden kalder event.set().

Undtagelseshåndtering og afslutning af tråde

Når undtagelser opstår i tråde, bliver de ikke direkte sendt til hovedtråden, så der er brug for et mønster til at fange og dele undtagelser.

 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)
  • Ved at placere undtagelser i en Queue og hente dem i hovedtråden, kan du pålideligt registrere fejl. Hvis du bruger concurrent.futures.ThreadPoolExecutor, kastes undtagelser videre med future.result(), hvilket gør dem nemmere at håndtere.

GIL (Global Interpreter Lock) og dets virkninger

På grund af mekanismen med GIL (Global Interpreter Lock) i CPython kører flere Python bytecodes faktisk ikke samtidigt i den samme proces. For opgaver, der er CPU-intensive, såsom tunge beregninger, anbefales det at bruge multiprocessing. På den anden side fungerer threading effektivt til I/O-baserede opgaver som f.eks. filaflæsning eller netværkskommunikation.

Sammendrag

Ved at bruge Pythons threading-modul kan du implementere multitrådede programmer og udføre flere processer samtidigt. Med synkroniseringsmekanismer som Lock og Condition kan du sikkert få adgang til delte ressourcer og udføre kompleks synkronisering. Desuden bliver trådhåndtering og effektiv parallel behandling lettere ved brug af daemon-tråde eller ThreadPoolExecutor.

Du kan følge med i ovenstående artikel ved hjælp af Visual Studio Code på vores YouTube-kanal. Husk også at tjekke YouTube-kanalen.

YouTube Video