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

Programski jezik python omogućuje korištenje višestruke obrade ili višenitnosti. U ovom ćete vodiču naučiti kako pisati višenitne aplikacije Python.

Što je nit?

Nit je jedinica izvršenja istovremenog programiranja. Multithreading je tehnika koja omogućuje CPU-u da izvršava mnoge zadatke jednog procesa u isto vrijeme. Te se niti mogu izvršavati pojedinačno dok dijele svoje resurse procesa.

Što je proces?

Proces je u osnovi program u izvođenju. Kada pokrenete aplikaciju na svom računalu (kao što je preglednik ili uređivač teksta), operativni sustav stvara a 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 dijeljenje informacija i komunikaciju unutar niti čini lakim i učinkovitim. Niti su lakše od procesa. Više niti se može izvršavati pojedinačno dok dijele svoje resurse procesa. Svrha multithreadinga je pokretanje više zadataka i funkcionalnih ćelija u isto vrijeme.

Š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

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

Svaki proces će imati 2 temeljne komponente:

  • Kodeks
  • Podatak

Sada, proces može sadržavati jedan ili više poddijelova tzv teme. To ovisi o arhitekturi OS-a. O niti možete razmišljati kao o dijelu procesa koji operacijski sustav može zasebno izvršiti.

Drugim riječima, to je tok uputa koje OS može neovisno pokrenuti. Niti unutar jednog procesa dijele podatke tog procesa i dizajnirane su da rade zajedno radi olakšavanja paralelizma.

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 MultiThreading

Python podržava konstrukcije za multiprocesiranje kao i za multithreading. U ovom vodiču prvenstveno ćete se usredotočiti na implementaciju višenitni aplikacije s pythonom. Postoje dva glavna modula koji se mogu koristiti za rukovanje nitima Python:

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

Međutim, u pythonu također postoji nešto što se zove globalno zaključavanje tumača (GIL). Ne dopušta mnogo povećanja performansi, a možda čak i 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 a 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 je ovdje pokriven 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

Naučit ćete više o uvjetima utrke i kako se nositi s njima u nadolazećim odjeljcima

Modul niti

OBJAŠNJENJE ŠIFRE

  1. Ove izjave uvoze modul vremena i niti koji se koriste za rukovanje izvršenjem i odgodom Python teme.
  2. Ovdje ste definirali funkciju tzv test_niti, koji će biti pozvan od strane započeti_novu_nit metoda. Funkcija pokreće while petlju za četiri iteracije i ispisuje naziv niti koja ju je pozvala. Nakon što je iteracija dovršena, ispisuje se poruka da je nit završila s izvođenjem.
  3. Ovo je glavni dio vašeg programa. Ovdje jednostavno nazovete započeti_novu_nit metoda s test_niti funkcija kao argument. Ovo će stvoriti novu nit za funkciju koju proslijedite kao argument i početi je izvršavati. Imajte na umu da možete zamijeniti ovo (nit_test) s bilo kojom drugom funkcijom koju želite pokrenuti kao nit.

Modul Threading

Ovaj modul je implementacija niti na visokoj razini u pythonu i de facto standard za upravljanje aplikacijama s više niti. Pruža širok raspon značajki u usporedbi s modulom niti.

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 objekata 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 počnete kodirati programe s više niti koristeći modul threading, ključno je razumjeti klasu Thread. Klasa thread je primarna klasa koja definira predložak i operacije niti u pythonu.

Najčešći način za stvaranje višenitne python aplikacije je deklaracija klase 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šenja ove aktivnosti i pridružiti() metoda se može koristiti za blokiranje svih ostalih kodova dok se trenutna aktivnost ne završi.

Pokušajmo sada upotrijebiti modul threadinga za implementaciju vašeg prethodnog primjera. Opet, zapalite svoje 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 naš prethodni primjer. Ovdje uvozite modul vremena i niti koji se koriste za rukovanje izvršenjem i kašnjenjima Python teme.
  2. U ovom dijelu stvarate klasu koja se zove threadtester, koja nasljeđuje ili proširuje Nit klasa modula navoja. Ovo je jedan od najčešćih načina stvaranja niti u pythonu. Međutim, trebali biste nadjačati samo 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 pri svakoj iteraciji i prolazi kroz ostatak koda dok i ne postane 0. U svakoj iteraciji ispisuje naziv niti koja se trenutno izvodi i spava 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()T metoda start se koristi za početak 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 od jedne niti pokuša promijeniti ili pristupiti podacima istovremeno, mogu se pojaviti pogreške.

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

Uvjeti zastoja i utrke

Prije nego što naučite o zastojima i uvjetima utrke, bilo bi korisno razumjeti nekoliko osnovnih definicija povezanih s paralelnim programiranjem:

  • Kritični odjeljak To je fragment koda koji pristupa ili mijenja zajedničke varijable i mora se izvesti kao atomska transakcija.
  • Prebacivanje konteksta To je proces koji CPU slijedi kako bi pohranio stanje niti prije promjene s jednog zadatka na drugi kako bi se kasnije mogao nastaviti s iste točke.

Zastoji

Zastoji su problem s kojim se programeri suočavaju kada pišu istodobne/višenitne aplikacije u pythonu. Najbolji način za razumijevanje zastoja je pomoću klasičnog primjera informatičkog 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

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

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

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

Ovo su dvije glavne klase problema koji se mogu pojaviti u višenitnoj ili distribuiranoj python aplikaciji. U sljedećem odjeljku naučit ćete kako prevladati ovaj problem sinkroniziranjem 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 2 stanja: zaključan or otključan. Podržava dvije metode:

  1. steći()Kada je stanje zaključavanja otključano, pozivanje metode Acquisi() će promijeniti stanje u zaključano i vratiti se. Međutim, ako je stanje zaključano, poziv za Acquisi() je blokiran dok neka druga nit ne pozove metodu release().
  2. oslobodi()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 zaključavanja u vašim aplikacijama. Zapalite svoje 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 znati da je brava stvarno radila? Ako pogledate izlaz, vidjet ćete da svaka od naredbi za ispis ispisuje točno jedan redak u isto vrijeme. Prisjetite se da su u ranijem primjeru izlazi iz ispisa bili slučajni jer je više niti istovremeno pristupalo metodi print(). Ovdje se funkcija ispisa poziva tek nakon što se postigne zaključavanje. Dakle, izlazi se prikazuju jedan po jedan i red po red.

Osim zaključavanja, 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 pythonovog GIL-a, definirajmo nekoliko pojmova koji će biti korisni u razumijevanju nadolazećeg odjeljka:

  1. Kod vezan za CPU: ovo se odnosi na bilo koji dio koda koji će CPU izravno izvršiti.
  2. I/O-vezan kod: ovo može biti bilo koji kod koji pristupa datotečnom sustavu kroz OS
  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) u pythonu je zaključavanje procesa ili mutex koji se koristi pri radu s procesima. Osigurava da jedna nit može pristupiti određenom resursu u isto vrijeme i također sprječava korištenje objekata i bajt kodova odjednom. To pogoduje jednonitnim programima u povećanju performansi. GIL u pythonu je vrlo jednostavan i lak za implementaciju.

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 je da koristi globalno zaključavanje na svakom procesu tumača, što znači da svaki proces tretira samog python tumača kao resurs.

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

  1. Python tumač stvara novi proces i stvara 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 tumaču u bilo kojem trenutku, što znači da će postojati samo jedna nit koja izvršava python kod u određenom trenutku.

To je u redu u procesoru s jednom jezgrom jer bi koristio vremensko odsijecanje (pogledajte prvi odjeljak ovog vodiča) za rukovanje nitima. Međutim, u slučaju višejezgrenih procesora, funkcija vezana za CPU koja se izvršava na više niti imat će značajan utjecaj na učinkovitost programa budući da zapravo neće koristiti sve dostupne jezgre u isto vrijeme.

Zašto je GIL bio potreban?

CPython skupljač smeća koristi učinkovitu tehniku ​​upravljanja memorijom poznatu kao brojanje referenci. Evo kako to funkcionira: svaki objekt u pythonu ima broj referenci, koji se povećava kada se dodijeli novom nazivu varijable ili doda u spremnik (poput torki, popisa itd.). Isto tako, broj referenci se smanjuje kada referenca izađe iz opsega ili kada se pozove naredba del. Kada broj referenci objekta dosegne 0, skuplja se 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 ovaj problem, programeri pythona odlučili su koristiti globalno zaključavanje tumača. Druga je opcija bila dodavanje zaključavanja svakom objektu što bi rezultiralo zastojima i povećanim opterećenjem zbog poziva () i release().

Stoga je GIL značajno ograničenje za višenitne python programe koji pokreću teške CPU-vezane operacije (što ih učinkovito čini jednonitnim). Ako želite koristiti više CPU jezgri u svojoj aplikaciji, koristite višeobradbeni modul umjesto toga.

Rezime

  • Python podržava 2 modula za višenitnost:
    1. __nit modul: pruža implementaciju niske razine za niti i zastario je.
    2. modul navoja: Pruža implementaciju visoke razine za višenitnost i trenutni je standard.
  • Da biste stvorili nit pomoću modula za izradu niti, morate učiniti sljedeće:
    1. Stvorite klasu koja proširuje Nit klase.
    2. Nadjačajte njegov konstruktor (__init__).
    3. Nadjačaj svoje trčanje() metoda.
    4. Kreirajte objekt ove klase.
  • Nit se može izvršiti pozivom početak() metoda.
  • The pridružiti() metoda se može koristiti za blokiranje drugih niti dok ova nit (ona na kojoj je pozvano pridruživanje) ne završi s izvođenjem.
  • Stanje utrke se događa kada više niti istovremeno pristupa ili mijenja dijeljeni resurs.
  • Može se izbjeći tako što Synchronizirajući niti.
  • Python podržava 6 načina za sinkronizaciju niti:
    1. Brave
    2. RLocks
    3. Semaphores
    4. Uvjeti
    5. Događaji, i
    6. Prepreke
  • Brave dopuštaju samo određenoj niti koja je dobila bravu da uđe u kritični odjeljak.
  • Zaključavanje ima 2 primarne metode:
    1. steći(): Postavlja stanje zaključavanja na zaključan. Ako se pozove na zaključani objekt, blokira se dok se resurs ne oslobodi.
    2. oslobodi(): Postavlja stanje zaključavanja na otključan i vraća se. Ako se pozove na otključanom objektu, vraća false.
  • Globalno zaključavanje tumača je mehanizam kroz koji samo 1 CPython proces tumača može se izvršiti odjednom.
  • Korišten je za olakšavanje funkcije brojanja referenci za CPythons-ov skupljač smeća.
  • Da bi Python aplikacije s velikim CPU vezanim operacijama, trebali biste koristiti višeprocesni modul.