Višenitnost u Python s primjerom: Naučite GIL u Python

⚡ Pametni sažetak

Višenitnost u Python pokreće nekoliko niti unutar jednog procesa tako da dijele memoriju i rade istovremeno. Modul za niti stvara i upravlja tim nitima, dok Global Interpreter Lock ograničava pravi paralelizam, što tehniku ​​čini najboljom za zadatke vezane uz ulaz/izlaz.

  • 🔘 Nit u odnosu na proces: Nit je jedinica izvršenja unutar procesa, a niti dijele podatke procesa dok se izvode pojedinačno.
  • ☑️ Dva modula: Zastarjeli modul _thread nudi pozive niske razine, dok je modul za izradu niti više razine moderni standard za izgradnju niti.
  • Stvori teme: Proširite klasu Thread, nadjačajte __init__ i run(), zatim pozovite start() za pokretanje i join() za čekanje završetka.
  • 🧪 Izbjegavajte uvjete utrke: Zaključavanje (lock) dopušta samo jednoj niti da uđe u kritični odjeljak istovremeno, sprječavajući zastoje i oštećene dijeljene podatke.
  • 🛠️ Razumjeti GIL: Globalna blokada interpretera omogućuje izvršavanje jedne niti Python bajtkod po bajtkod, tako da rad vezan uz CPU zahtijeva višestruko procesiranje.
  • 🤖 Radna opterećenja umjetne inteligencije: Višenitnost ubrzava ulazno/izlazne korake umjetne inteligencije poput učitavanja podataka, dok NumPy i PyTorch oslobodi GIL za paralelno računanje.

Višenitnost u Python

The Python Programski jezik vam omogućuje korištenje višestruke obrade ili višenitnosti. U ovom tutorialu naučit ćete kako pisati višenitne aplikacije u Python.

Što je nit?

Nit je jedinica izvršavanja u konkurentnom programiranju. Višenitnost je tehnika koja omogućuje CPU-u da istovremeno izvršava više zadataka jednog procesa. Te niti mogu se izvršavati pojedinačno dok dijele svoje procesne resurse.

Što je proces?

Proces je u osnovi program koji se izvršava. Kada pokrenete aplikaciju na računalu (poput preglednika ili uređivača teksta), operativni sustav stvara proces.

U čemu je Multithreading Python?

Višenitnost u Python Programiranje je dobro poznata tehnika u kojoj više niti u procesu dijeli svoj podatkovni prostor s glavnom niti, što olakšava i čini učinkovitim dijeljenje informacija i komunikaciju unutar niti. Niti su lakše od procesa. Više niti može se izvršavati pojedinačno dok dijeli resurse procesa. Svrha višenitnosti je istovremeno izvršavanje više zadataka i funkcija.

Što je višeprocesiranje?

višeobradbeni omogućuje vam pokretanje više nepovezanih procesa istovremeno. Ovi procesi ne dijele svoje resurse i komuniciraju putem IPC-a.

Python Multithreading vs Multiprocessing

Za razumijevanje procesa i niti, razmotrite ovaj scenarij: .exe datoteka na vašem računalu je program. Kada je otvorite, OS je učitava u memoriju, a CPU je izvršava. Instanca programa koja se sada izvršava naziva se proces.

Svaki proces ima dvije osnovne komponente:

  • The Code
  • Podatak

Sada, proces može sadržavati jedan ili više poddijelova tzv teme. To ovisi o arhitekturi operacijskog sustava. Nit možete zamisliti kao dio procesa koji operacijski sustav može zasebno izvršiti.

Drugim riječima, to je niz instrukcija koje OS može izvršavati neovisno. Niti unutar jednog procesa dijele podatke tog procesa i dizajnirane su za zajednički rad kako bi olakšale paralelizam.

Zašto koristiti Multithreading?

Multithreading vam omogućuje rastavljanje aplikacije na više podzadataka i pokretanje tih zadataka istovremeno. Ako pravilno koristite multithreading, brzina vaše aplikacije, performanse i renderiranje mogu se poboljšati.

Python Višenitnost

Python podržava konstrukcije za višestruku obradu i višenitnost. U ovom vodiču ćete se prvenstveno usredotočiti na implementaciju višenitni aplikacije sa PythonPostoje dva glavna modula koja se mogu koristiti za rukovanje nitima u Python:

  1. The nit modul, i
  2. The threading modul

Međutim, u Python, postoji i nešto što se zove globalno zaključavanje interpretera (GIL). Ne dopušta veliko poboljšanje performansi i može čak smanjiti performanse nekih višenitnih aplikacija. Naučit ćete sve o tome u sljedećim odjeljcima ovog vodiča.

Moduli Thread i Threading

Dva modula o kojima ćete naučiti u ovom vodiču su modul niti i modul navoja.

Međutim, modul niti je odavno zastario. Počevši od Python 3, označen je kao zastario i dostupan je samo kao _nit za kompatibilnost unazad.

Trebali biste koristiti višu razinu threading modul za aplikacije koje namjeravate implementirati. Modul niti ovdje je obrađen samo u obrazovne svrhe.

Modul niti

Sintaksa za stvaranje nove niti pomoću ovog modula je sljedeća:

thread.start_new_thread(function_name, arguments)

U redu, sada ste pokrili osnovnu teoriju za početak kodiranja. Dakle, otvorite svoje IDLE ili bilježnicu i upišite sljedeće:

import time
import _thread

def thread_test(name, wait):
   i = 0
   while i <= 3:
      time.sleep(wait)
      print("Running %s\n" %name)
      i = i + 1

   print("%s has finished execution" %name)

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))

Spremite datoteku i pritisnite F5 za pokretanje programa. Ako je sve učinjeno ispravno, ovo je rezultat koji biste trebali vidjeti:

Modul niti

Više o uvjetima utrke i kako se s njima nositi saznat ćete u sljedećim odjeljcima.

Modul niti

OBJAŠNJENJE ŠIFRE

  1. Ove naredbe uvoze module vremena i niti, koji se koriste za rukovanje izvršavanjem i odgađanjem Python teme.
  2. Ovdje ste definirali funkciju tzv test_niti, koji će biti pozvan od strane započeti_novu_nit metoda. Funkcija izvršava while petlju tijekom četiri iteracije i ispisuje naziv niti koja ju je pozvala. Nakon što je iteracija završena, ispisuje poruku da je nit završila izvršavanje.
  3. Ovo je glavni dio vašeg programa. Ovdje jednostavno nazovete započeti_novu_nit metoda s test_niti funkciju kao argument. Ovo će stvoriti novu nit za funkciju koju prosljeđujete kao argument i početi je izvršavati. Imajte na umu da ovo (thread_test) možete zamijeniti bilo kojom drugom funkcijom koju želite pokrenuti kao nit.

Modul Threading

Ovaj modul je implementacija visoke razine niti u Python i de facto standard za upravljanje višenitnim aplikacijama. Pruža širok raspon značajki u usporedbi s modulom thread.

Struktura Threading modula

Struktura Threading modula

Ovdje je popis nekih korisnih funkcija definiranih u ovom modulu:

Naziv funkcije Description
activeCount() Vraća broj od Nit predmeti koji su još uvijek živi.
trenutna nit() Vraća trenutni objekt klase Thread.
nabrojati() Ispisuje sve aktivne Thread objekte.
isDaemon() Vraća true ako je nit demon.
živ je() Vraća true ako je nit još živa.
Thread Metode klase
početak() Pokreće aktivnost niti. Mora se pozvati samo jednom za svaku nit jer će izbaciti pogrešku vremena izvođenja ako se pozove više puta.
trčanje() Ova metoda označava aktivnost niti i može je nadjačati klasa koja proširuje klasu Thread.
pridružiti() Blokira izvođenje drugog koda sve dok se nit na kojoj je pozvana metoda join() ne prekine.

Backstory: The Thread Class

Prije nego što počnete kodirati višenitne programe pomoću modula za threading, ključno je razumjeti klasu Thread. Klasa thread je primarna klasa koja definira predložak i operacije niti u Python.

Najčešći način stvaranja višenitnog Python Cilj aplikacije je deklarirati klasu koja proširuje klasu Thread i nadjačava njezinu metodu run().

Klasa Thread, ukratko, označava sekvencu koda koja se izvodi zasebno nit kontrole.

Dakle, kada pišete višenitnu aplikaciju, učinit ćete sljedeće:

  1. definirajte klasu koja proširuje klasu Thread
  2. Nadjačaj __init__ konstruktor
  3. Nadjačaj trčanje() način

Nakon što je objekt niti napravljen, početak() metoda se može koristiti za početak izvršavanja ove aktivnosti i pridružiti() metoda se može koristiti za blokiranje svih ostalih kodova dok se trenutna aktivnost ne završi.

Sada pokušajmo upotrijebiti modul za threading kako bismo implementirali vaš prethodni primjer. Ponovno pokrenite IDLE i upišite sljedeće:

import time
import threading

class threadtester (threading.Thread):
    def __init__(self, id, name, i):
       threading.Thread.__init__(self)
       self.id = id
       self.name = name
       self.i = i
       
    def run(self):
       thread_test(self.name, self.i, 5)
       print ("%s has finished execution " %self.name)

def thread_test(name, wait, i):

    while i:
       time.sleep(wait)
       print ("Running %s \n" %name)
       i = i - 1

if __name__=="__main__":
    thread1 = threadtester(1, "First Thread", 1)
    thread2 = threadtester(2, "Second Thread", 2)
    thread3 = threadtester(3, "Third Thread", 3)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

Ovo će biti rezultat kada izvršite gornji kod:

Backstory: The Thread Class

OBJAŠNJENJE ŠIFRE

Backstory: The Thread Class

  1. Ovaj dio je isti kao i naš prethodni primjer. Ovdje uvozite modul vremena i niti, koji se koriste za rukovanje izvršavanjem i kašnjenjima Python teme.
  2. U ovom dijelu stvarate klasu koja se zove threadtester, koja nasljeđuje ili proširuje Nit klasa modula za izradu niti. Ovo je jedan od najčešćih načina stvaranja niti u PythonMeđutim, trebali biste samo nadjačati konstruktor i trčanje() način u vašoj aplikaciji. Kao što možete vidjeti u gornjem uzorku koda, __init__ metoda (konstruktor) je nadjačana. Slično tome, također ste nadjačali trčanje() metoda. Sadrži kod koji želite izvršiti unutar niti. U ovom primjeru pozvali ste funkciju thread_test().
  3. Ovo je metoda thread_test() koja uzima vrijednost i kao argument, smanjuje ga za 1 u svakoj iteraciji i prolazi kroz ostatak koda dok i ne postane 0. U svakoj iteraciji ispisuje naziv niti koja se trenutno izvršava i miruje nekoliko sekundi čekanja (što se također uzima kao argument).
  4. thread1 = threadtester(1, “First Thread”, 1) Ovdje stvaramo nit i prosljeđujemo tri parametra koja smo deklarirali u __init__. Prvi parametar je ID niti, drugi parametar je naziv niti, a treći parametar je brojač, koji određuje koliko puta bi se trebala pokrenuti while petlja.
  5. thread2.start() Metoda start se koristi za pokretanje izvršavanja niti. Interno, funkcija start() poziva metodu run() vaše klase.
  6. thread3.join() Metoda join() blokira izvođenje drugog koda i čeka dok nit na kojoj je pozvana završi.

Kao što već znate, niti koje su u istom procesu imaju pristup memoriji i podacima tog procesa. Kao rezultat toga, ako više niti pokuša istovremeno promijeniti ili pristupiti podacima, mogu se pojaviti pogreške.

U sljedećem odjeljku vidjet ćete različite vrste komplikacija koje se mogu pojaviti kada niti pristupaju podacima i kritičnom dijelu bez provjere postojećih transakcija pristupa.

Uvjeti zastoja i utrke

Prije učenja o zastojima i uvjetima utrke, korisno je razumjeti nekoliko osnovnih definicija vezanih uz konkurentno programiranje:

  • Kritični odjeljak: To je fragment koda koji pristupa ili mijenja dijeljene varijable i mora se izvršiti kao atomska transakcija.
  • Prekidač konteksta: To je proces koji CPU slijedi kako bi pohranio stanje niti prije promjene s jednog zadatka na drugi, tako da se kasnije može nastaviti s iste točke.

Zastoji

Zastoji su najopasniji problem s kojim se programeri suočavaju prilikom pisanja istovremenih/višenitnih aplikacija u PythonNajbolji način za razumijevanje zastoja je korištenje klasičnog primjera računalnog problema poznatog kao Objed Philosophers Problem.

Izjava o problemu za filozofe objedovanja je sljedeća:

Pet filozofa sjedi za okruglim stolom s pet tanjura špageta (vrsta tjestenine) i pet vilica, kao što je prikazano na dijagramu.

Objed Philosophers Problem

Objed Philosophers Problem

U bilo kojem trenutku, filozof mora ili jesti ili razmišljati.

Štoviše, filozof mora uzeti dvije vilice koje su mu susjedne (tj. lijevu i desnu vilicu) prije nego što može jesti špagete. Problem zastoja javlja se kada svih pet filozofa istovremeno podignu svoje desne vilice.

Budući da svaki od filozofa ima jednu vilicu, svi će čekati da ostali odlože vilicu. Kao rezultat toga, nitko od njih neće moći jesti špagete.

Slično, u konkurentnom sustavu, zastoj se događa kada različite niti ili procesi (filozofi) pokušavaju steći dijeljene resurse sustava (forkovi) u isto vrijeme. Kao rezultat toga, niti jedan od procesa nema priliku za izvršenje jer čekaju drugi resurs koji drži neki drugi proces.

Uvjeti utrke

Uvjet utrke je neželjeno stanje programa koje se javlja kada sustav istovremeno izvršava dvije ili više operacija. Na primjer, razmotrimo ovu jednostavnu for petlju:

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

Ako stvarate n Broj niti koje istovremeno izvršavaju ovaj kod, ne možete odrediti vrijednost i (koju dijele niti) kada program završi s izvršavanjem. To je zato što se u stvarnom okruženju s više niti niti mogu preklapati, a vrijednost i koju je nit dohvatila i izmijenila može se promijeniti između kada joj pristupi neka druga nit.

Ovo su dvije glavne klase problema koje se mogu pojaviti u višenitnom ili distribuiranom Python aplikacija. U sljedećem odjeljku naučit ćete kako prevladati ovaj problem sinkronizacijom niti.

Synchronizirajući niti

Za rješavanje uvjeta utrke, zastoja i drugih problema temeljenih na nitima, modul za niti pruža Zaključati objekt. Ideja je da kada nit želi pristup određenom resursu, dobiva zaključavanje za taj resurs. Jednom kada nit zaključa određeni resurs, nijedna druga nit mu ne može pristupiti dok se zaključavanje ne oslobodi. Kao rezultat toga, promjene resursa bit će atomske, a uvjeti utrke bit će izbjegnuti.

Zaključavanje je primitiv sinkronizacije niske razine implementiran od strane _nit modul. U bilo kojem trenutku, brava može biti u jednom od dva stanja: zaključan or otključan. Podržava dvije metode:

  1. steći(): Kada je stanje zaključavanja otključano, pozivanje metode acquire() promijenit će stanje u zaključano i vratiti . Međutim, ako je stanje zaključano, poziv metode acquire() je blokiran dok neka druga nit ne pozove metodu release().
  2. otpuštanje(): Metoda release() koristi se za postavljanje stanja na otključano, tj. za otključavanje. Može se pozvati bilo kojom niti, ne nužno onom koja je stekla zaključavanje.

Evo primjera korištenja brava u vašim aplikacijama. Pokrenite IDLE i upišite sljedeće:

import threading
lock = threading.Lock()

def first_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the first funcion')
        lock.release()

def second_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the second funcion')
        lock.release()

if __name__=="__main__":
    thread_one = threading.Thread(target=first_function)
    thread_two = threading.Thread(target=second_function)

    thread_one.start()
    thread_two.start()

    thread_one.join()
    thread_two.join()

Sada pritisnite F5. Trebali biste vidjeti ovakav izlaz:

Synchroniziranje niti

OBJAŠNJENJE ŠIFRE

Synchroniziranje niti

  1. Ovdje jednostavno stvarate novu bravu pozivom threading.Lock() tvornička funkcija. Interno, Lock() vraća instancu najučinkovitije konkretne klase Lock koju održava platforma.
  2. U prvoj izjavi zaključavanje dobivate pozivanjem metode Acquisi(). Kada je zaključavanje odobreno, ispisujete "zaključavanje stečeno" na konzolu. Nakon što sav kod za koji želite da se nit pokrene završi s izvršenjem, otključavate zaključavanje pozivanjem metode release().

Teorija je u redu, ali kako znate da je brava stvarno radila? Ako pogledate izlaz, vidjet ćete da svaka od naredbi print ispisuje točno jedan redak u isto vrijeme. Podsjetimo se da su u ranijem primjeru izlazi iz printa bili nasumični jer je više niti istovremeno pristupalo metodi print(). Ovdje se funkcija print poziva tek nakon što je brava stečena. Dakle, izlazi se prikazuju jedan po jedan i redak po redak.

Osim brava, Python također podržava neke druge mehanizme za rukovanje sinkronizacijom niti, kao što je navedeno u nastavku:

  1. RLocks
  2. Semaphores
  3. Uvjeti
  4. Događaji, i
  5. Prepreke

Globalno zaključavanje tumača (i kako to riješiti)

Prije nego što uđemo u detalje PythonŠto se tiče GIL-a, definirajmo nekoliko pojmova koji će biti korisni za razumijevanje sljedećeg odjeljka:

  1. Kod vezan uz CPU: ovo se odnosi na bilo koji dio koda koji će izravno izvršavati CPU.
  2. I/O kod: to može biti bilo koji kod koji pristupa datotečnom sustavu putem OS-a.
  3. CPython: to je referenca izvršenje of Python i može se opisati kao tumač napisan u C i Python (programski jezik).

U čemu je GIL Python?

Globalna brava tumača (GIL) in Python je brava procesa ili mutex koji se koristi pri radu s procesima. Osigurava da jedna nit može pristupiti određenom resursu u određenom trenutku, a također sprječava istovremenu upotrebu objekata i bajtkodova. To koristi programima s jednom niti povećanjem performansi. GIL u Python vrlo je jednostavan i lako se provodi.

Zaključavanje se može koristiti kako bi se osiguralo da samo jedna nit ima pristup određenom resursu u određenom trenutku.

Jedna od značajki Python jest da koristi globalno zaključavanje na svakom procesu interpretatora, što znači da svaki proces tretira Python sam tumač kao resurs.

Na primjer, pretpostavimo da ste napisali Python program koji koristi dvije niti za izvođenje i CPU i 'I/O' operacija. Kada izvršite ovaj program, događa se sljedeće:

  1. The Python Interpreter stvara novi proces i pokreće niti.
  2. Kada se nit-1 pokrene, prvo će nabaviti GIL i zaključati ga.
  3. Ako se nit-2 sada želi izvršiti, morat će pričekati da se GIL oslobodi čak i ako je drugi procesor slobodan.
  4. Sada, pretpostavimo da thread-1 čeka I/O operaciju. U to će vrijeme osloboditi GIL, a thread-2 će ga preuzeti.
  5. Nakon dovršetka I/O operacija, ako se nit-1 sada želi izvršiti, ponovno će morati pričekati da nit-2 oslobodi GIL.

Zbog toga, samo jedna nit može pristupiti interpreteru u bilo kojem trenutku, što znači da će se izvršavati samo jedna nit. Python kod u određenom trenutku.

To je u redu u jednojezgrenom procesoru jer bi se koristilo određivanje vremena (pogledajte prvi odjeljak ovog vodiča) za rukovanje nitima. Međutim, u slučaju višejezgrenih procesora, funkcija vezana uz CPU koja se izvršava na više niti imat će značajan utjecaj na učinkovitost programa jer zapravo neće koristiti sve dostupne jezgre istovremeno.

Zašto je GIL bio potreban?

CPython Sakupljač smeća koristi učinkovitu tehniku ​​upravljanja memorijom poznatu kao brojanje referenci. Evo kako to funkcionira: Svaki objekt u Python ima broj referenci koji se povećava kada se dodijeli novom nazivu varijable ili doda u spremnik (poput n-torki, popisa itd.). Slično tome, broj referenci se smanjuje kada referenca izađe izvan dosega ili kada se pozove naredba del. Kada broj referenci objekta dosegne 0, on se skuplja kao smeće, a dodijeljena memorija se oslobađa.

Ali problem je u tome što je varijabla broja referenci sklona uvjetima utrke kao i svaka druga globalna varijabla. Kako bi riješili taj problem, programeri Python odlučili su koristiti globalno zaključavanje interpretera. Druga mogućnost bila je dodati zaključavanje svakom objektu, što bi rezultiralo zastojima i povećanim opterećenjem od poziva acquire() i release().

Stoga je GIL značajno ograničenje za višenitne Python programe koji izvršavaju operacije koje zahtijevaju puno prostora na CPU-u (što ih zapravo čini jednonitnima). Ako želite koristiti više jezgri CPU-a u svojoj aplikaciji, upotrijebite višeobradbeni modul umjesto toga.

Pitanja i odgovori

Koristite niti za rad vezan uz ulazno/izlazne operacije poput mrežnih poziva, pristupa datotekama ili upita bazi podataka, gdje niti čekaju na vanjske resurse. Koristite višestruku obradu za rad vezan uz CPU poput teških izračuna, jer se izvodi na više jezgri i zaobilazi GIL.

Ne po zadanim postavkama. PEP 703 uveo je opcionalnu ugradnju sa slobodnim nitima Python 3.13 koji onemogućuje GIL i Python 3.14 ga nastavlja. Standardne verzije i dalje isporučuju GIL, tako da većina programa nastavlja raditi točno kao i prije.

Daemon nit se izvršava u pozadini i ne blokira izlazak programa. Kada ostanu samo daemon niti, Python isključuje ih i završava. Postavite ga s thread.daemon = True prije pozivanja start().

Klasa Thread ne vraća izravno rezultat run(). Pohranite vrijednost na objekt thread ili dijeljeni red čekanja ili upotrijebite concurrent.futures.ThreadPoolExecutor, čija metoda submit() vraća Future koji čitate s result().

ThreadPoolExecutor je pomoćnik visoke razine koji upravlja skupom radnih niti umjesto vas. Zadatke šaljete pomoću submit() ili map(), a on obrađuje stvaranje niti, ponovnu upotrebu i čišćenje, vraćajući Future objekte koji sadrže svaki rezultat.

Možete stvoriti stotine ili tisuće niti, ali GIL dopušta izvršavanje samo jedne Python bajtkod u bilo kojem trenutku. I dalje se preklapaju tijekom čekanja na ulazno/izlazne operacije, tako da mnoge istovremene niti puno više pomažu programima vezanim za ulazno/izlazne operacije nego onima vezanim za CPU.

Da, uglavnom za korake vezane uz ulazno/izlazne operacije poput učitavanja podataka, pozivanja API-ja ili čitanja datoteka. Teško treniranje vezano je uz procesor, pa biblioteke poput NumPy i PyTorch osloboditi GIL u C kodu ili se osloniti na multiprocesiranje za pravi paralelizam.

Da. GitHub Copilot automatski dovršava uobičajene obrasce kao što su podklase Thread-a, korištenje Lock-a i postavke ThreadPoolExecutor-a iz komentara ili naziva funkcije. Uvijek pregledajte generirani kod za uvjete utrke i ispravno rukovanje zaključavanjem prije nego što se na njega oslonite.

Sažmite ovu objavu uz: