`threading`-modulen i Python

`threading`-modulen i Python

Den här artikeln förklarar threading-modulen i Python.

YouTube Video

threading-modulen i Python

threading-modulen i Python är ett standardbibliotek som stöder flertrådsprogrammering. Genom att använda trådar kan flera processer köras samtidigt, vilket kan förbättra prestandan hos program, särskilt vid blockerande operationer som I/O-väntetider. På grund av Pythons Global Interpreter Lock (GIL) är effektiviteten hos multitrådning begränsad för CPU-intensiva operationer, men det fungerar effektivt för I/O-intensiva operationer.

Följande avsnitt förklarar grunderna i att använda threading-modulen och hur man kontrollerar trådar.

Grundläggande användning av trådar

Skapa och köra trådar

För att skapa en tråd och utföra parallell bearbetning, använd klassen threading.Thread. Ange målfunktionen för att skapa en tråd och köra den tråden.

 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 detta exempel körs funktionen worker i en separat tråd, medan huvudtråden fortsätter att fungera. Genom att anropa metoden join() väntar huvudtråden på att undertråden ska slutföras.

Namngivning av trådar

Att ge trådar meningsfulla namn gör loggning och felsökning enklare. Du kan ange det med argumentet 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() returnerar en lista över aktuella trådar, vilket är användbart för felsökning och övervakning av tillstånd.

Ärva trådklassen

Om du vill anpassa den tråd-körande klassen kan du definiera en ny klass genom att ärva klassen threading.Thread.

 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 det här exemplet åsidosätts metoden run() för att definiera trådens beteende, vilket gör att varje tråd kan behålla sin egen data. Detta är användbart när trådar utför komplex bearbetning eller när du vill att varje tråd ska ha sin egen oberoende data.

Synkronisering mellan trådar

När flera trådar får åtkomst till delade resurser samtidigt kan datakörning inträffa. För att förhindra detta tillhandahåller threading-modulen flera synkroniseringsmekanismer.

Lås (Lock)

Lock-objektet används för att implementera exklusiv kontroll av resurser mellan trådar. Medan en tråd låser en resurs kan andra trådar inte få åtkomst till den resursen.

 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 detta exempel får fem trådar åtkomst till en delad resurs, men Lock används för att förhindra att flera trådar ändrar data samtidigt.

Återinträdbart lås (RLock)

Om samma tråd behöver ta ett lås flera gånger, använd ett RLock (återinträdbart lås). Detta är användbart för rekursiva anrop eller för bibliotek som kan ta lås över olika anrop.

 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 samma tråd åter ta ett lås den redan håller, vilket hjälper till att förhindra dödlägen vid nästlade låsanrop.

Villkor (Condition)

Condition används för att låta trådar vänta tills ett specifikt villkor är uppfyllt. När en tråd uppfyller ett villkor kan du anropa notify() för att meddela en annan tråd, eller notify_all() för att meddela alla väntande trådar.

Nedan är ett exempel med en producent och konsument som använder ett Condition-objekt.

 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()
  • Denna kod använder ett Condition så att producenten meddelar när data har lagts till, och konsumenten väntar på detta meddelande innan den hämtar data, vilket uppnår synkronisering.

Daemonisering av trådar

Daemontrådar avslutas tvångsmässigt när huvudtråden slutar. Medan vanliga trådar måste vänta på att avslutas, avslutas daemontrådar automatiskt.

 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 det här exemplet är worker-tråden daemoniserad, så den avslutas tvångsmässigt när huvudtråden slutar.

Hantering av trådar med ThreadPoolExecutor

Förutom threading-modulen kan du använda ThreadPoolExecutor från concurrent.futures-modulen för att hantera en pool av trådar och köra uppgifter parallellt.

 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 skapar en trådpool och hanterar uppgifter effektivt. Ange antalet trådar som ska köras samtidigt med max_workers.

Händelsekommunikation mellan trådar

Genom att använda threading.Event kan du sätta flaggor mellan trådar för att meddela andra trådar om att en händelse inträffat.

 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
  • Denna kod visar en mekanism där arbetstråden väntar på Event-signalen och återupptar bearbetningen när huvudtråden anropar event.set().

Undantagshantering och trådterminering i trådar

När undantag uppstår i trådar vidarebefordras de inte direkt till huvudtråden, så ett mönster behövs för att fånga och dela undantag.

 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)
  • Genom att lägga undantag i en Queue och hämta dem i huvudtråden kan du pålitligt upptäcka fel. Om du använder concurrent.futures.ThreadPoolExecutor kastas undantag igen med future.result(), vilket gör dem enklare att hantera.

GIL (Global Interpreter Lock) och dess effekter

På grund av mekanismen med GIL (Global Interpreter Lock) i CPython körs inte flera Python-bytekoder faktiskt samtidigt inom samma process. För uppgifter som är CPU-intensiva, till exempel tunga beräkningar, rekommenderas det att använda multiprocessing. Å andra sidan fungerar threading effektivt för I/O-bundna uppgifter såsom filläsning eller nätverkskommunikation.

Sammanfattning

Med Pythons threading-modul kan du implementera flertrådade program och köra flera processer samtidigt. Med synkroniseringsmekanismer som Lock och Condition kan du säkert komma åt delade resurser och utföra komplex synkronisering. Dessutom blir trådhantering och effektiv parallell bearbetning enklare med daemontrådar eller ThreadPoolExecutor.

Du kan följa med i artikeln ovan med hjälp av Visual Studio Code på vår YouTube-kanal. Vänligen kolla även in YouTube-kanalen.

YouTube Video