Programovací jazyk python vám umožňuje používať multiprocesing alebo multithreading. V tomto tutoriále sa naučíte písať viacvláknové aplikácie v Pythone.
Čo je to vlákno?
Vlákno je jednotka exekúcie pri súbežnom programovaní. Multithreading je technika, ktorá umožňuje CPU vykonávať veľa úloh jedného procesu súčasne. Tieto vlákna sa môžu vykonávať jednotlivo a zdieľať svoje procesné prostriedky.
Čo je to proces?
Proces je v podstate program vo vykonávaní. Pri spustení aplikácie v počítači (napríklad v prehliadači alebo textovom editore) vytvorí operačný systém proces.
Čo je to multithreading v Pythone?
Multithreading v programovaní v Pythone je známa technika, pri ktorej viacnásobné vlákna v procese zdieľajú svoj dátový priestor s hlavným vláknom, vďaka čomu je zdieľanie informácií a komunikácia v rámci vlákien ľahká a efektívna. Nite sú ľahšie ako procesy. Viaceré vlákna sa môžu vykonávať jednotlivo, zatiaľ čo zdieľajú svoje zdroje procesu. Účelom multithreadingu je spustenie viacerých úloh a funkčných buniek súčasne.
Čo je to multiprocesing?
Multiprocesing vám umožňuje spustiť niekoľko nesúvisiacich procesov súčasne. Tieto procesy nezdieľajú svoje zdroje a komunikujú prostredníctvom IPC.
Python Multithreading vs Multiprocessing
Aby ste pochopili procesy a vlákna, zvážte tento scenár: Súbor .exe vo vašom počítači je program. Keď ho otvoríte, OS ho načíta do pamäte a CPU ho vykoná. Inštancia programu, ktorý je teraz spustený, sa nazýva proces.
Každý proces bude mať 2 základné komponenty:
- Kód
- Dáta
Proces teraz môže obsahovať jednu alebo viac čiastkových častí nazývaných vlákna. To záleží na OS architektúre .Môžete myslieť na vlásku ako časti procesu, ktorý môže byť vykonaný oddelene od operačného systému.
Inými slovami, je to prúd pokynov, ktoré môže OS spustiť nezávisle. Vlákna v rámci jedného procesu zdieľajú údaje tohto procesu a sú navrhnuté tak, aby spolupracovali na uľahčení paralelizmu.
V tomto návode sa dozviete,
- Čo je to vlákno?
- Čo je to proces?
- Čo je to multithreading?
- Čo je to multiprocesing?
- Python Multithreading vs Multiprocessing
- Prečo používať viacvláknové spracovanie?
- Python MultiThreading
- Moduly Thread a Threading
- Vláknový modul
- Závitový modul
- Zablokovanie a podmienky pretekov
- Synchronizácia vlákien
- Čo je to GIL?
- Prečo bol GIL potrebný?
Prečo používať viacvláknové spracovanie?
Multithreading vám umožňuje rozdeliť aplikáciu na viac čiastkových úloh a spustiť tieto úlohy súčasne. Ak správne používate viacvláknové spracovanie, dá sa zlepšiť rýchlosť, výkon a vykreslenie vašej aplikácie.
Python MultiThreading
Python podporuje konštrukty pre viacprocesové aj viacvláknové spracovanie. V tomto tutoriáli sa budete primárne zameriavať na implementáciu viacvláknových aplikácií s pythonom. Existujú dva hlavné moduly, ktoré možno použiť na spracovanie vlákien v Pythone:
- Niť modul, a
- Threading modul
V pythone však existuje aj niečo, čo sa nazýva globálny zámok tlmočníka (GIL). Neumožňuje veľké zvýšenie výkonu a môže dokonca znížiť výkon niektorých viacvláknových aplikácií. Všetko sa dozviete v nasledujúcich častiach tohto tutoriálu.
Moduly Thread a Threading
Dva moduly, o ktorých sa v tejto príručke dozviete, sú vláknový modul a závitový modul .
Modul vlákna je však už dávno zastaraný. Počnúc Pythonom 3 bol označený ako zastaraný a pre spätnú kompatibilitu je prístupný iba ako __thread .
Pre aplikácie, ktoré chcete nasadiť, by ste mali používať modul vlákien vyššej úrovne . Vláknový modul tu bol pokrytý iba na vzdelávacie účely.
Vláknový modul
Syntax pre vytvorenie nového vlákna pomocou tohto modulu je nasledovná:
thread.start_new_thread(function_name, arguments)
Dobre, teraz ste prebrali základnú teóriu na začatie kódovania. Takže otvorte IDLE alebo poznámkový blok a zadajte nasledovné:
import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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))
Uložte súbor a stlačením klávesu F5 spustite program. Ak bolo všetko vykonané správne, mali by ste vidieť tento výstup:
Viac o podmienkach pretekov a o tom, ako s nimi zaobchádzať, sa dozviete v nasledujúcich častiach
VYSVETLENIE KÓDU
- Tieto príkazy importujú modul času a vlákna, ktorý sa používa na spracovanie vykonania a oneskorenia vlákien Pythonu.
- Tu ste definovali funkciu nazvanú thread_test, ktorá sa bude volať metódou start_new_thread . Funkcia spustí chvíľu cyklu pre štyri iterácie a vytlačí názov vlákna, ktoré ju volalo. Po dokončení iterácie sa vytlačí správa s oznámením, že vlákno skončilo vykonávanie.
- Toto je hlavná časť vášho programu. Tu jednoducho zavoláte metódu start_new_thread s funkciou thread_test ako argument.
Týmto vytvoríte nové vlákno pre funkciu, ktorú odovzdáte ako argument, a začnete ju vykonávať. Toto ( test vlákna _ ) môžete nahradiť akoukoľvek inou funkciou, ktorú chcete spustiť ako vlákno.
Závitový modul
Tento modul predstavuje implementáciu vlákien na vysokej úrovni v pythone a de facto štandard pre správu viacvláknových aplikácií. V porovnaní s vláknovým modulom poskytuje širokú škálu funkcií.

Tu je zoznam niektorých užitočných funkcií definovaných v tomto module:
Názov funkcie | Popis |
activeCount () | Vráti počet objektov vlákna, ktoré sú stále nažive |
currentThread () | Vráti aktuálny objekt triedy Thread. |
vymenovať () | Uvádza zoznam všetkých aktívnych objektov vlákien. |
isDaemon () | Vráti hodnotu true, ak je vlákno démon. |
je nažive() | Ak je vlákno stále živé, vráti hodnotu true. |
Metódy triedy vlákna | |
štart () | Spustí činnosť vlákna. Musí sa volať iba raz pre každé vlákno, pretože pri opakovanom volaní vyvolá chybu za behu. |
run () | Táto metóda označuje aktivitu vlákna a môže byť prepísaná triedou, ktorá rozširuje triedu vlákna. |
pripojiť sa () | Blokuje vykonávanie iného kódu, kým nebude ukončené vlákno, na ktorom bola volaná metóda join (). |
Backstory: Trieda vlákna
Predtým, ako začnete programovať viacvláknové programy pomocou modulu vlákien, je dôležité pochopiť triedu vlákien. Trieda vlákien je primárna trieda, ktorá definuje šablónu a operácie vlákna v pythone.
Najbežnejším spôsobom, ako vytvoriť aplikáciu s viacvláknovým pythonom, je deklarovať triedu, ktorá rozširuje triedu Thread a prepíše jej metódu run ().
Trieda Thread v súhrne znamená sekvenciu kódov, ktorá sa spúšťa v samostatnom vlákne kontroly.
Pri písaní viacvláknovej aplikácie teda budete robiť nasledovné:
- definovať triedu, ktorá rozširuje triedu vlákien
- Prepísať konštruktor __init__
- Prepísať metódu run ()
Po vytvorení objektu vlákna je možné na začatie vykonávania tejto aktivity použiť metódu start () a metódu join () možno použiť na blokovanie všetkých ostatných kódov, kým sa neukončí aktuálna aktivita.
Teraz skúsime pomocou modulu threading implementovať váš predchádzajúci príklad. Opäť spustite IDLE a zadajte nasledovné:
import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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()
Toto bude výstup, keď vykonáte vyššie uvedený kód:
VYSVETLENIE KÓDU
- Táto časť je rovnaká ako v predchádzajúcom príklade. Tu importujete modul času a vlákna, ktorý sa používa na spracovanie a oneskorenie vlákien Pythonu.
- V tomto bite vytvárate triedu nazvanú threadtester, ktorá dedí alebo rozširuje triedu Thread modulu závitov. Toto je jeden z najbežnejších spôsobov vytvárania vlákien v pythone. Mali by ste však vo svojej aplikácii prepísať iba konštruktor a metódu run () . Ako vidíte vo vyššie uvedenej ukážke kódu, metóda (konštruktor) __init__ bola prepísaná.
Podobne ste tiež prepísali metódu run () . Obsahuje kód, ktorý chcete vykonať vo vnútri vlákna. V tomto príklade ste volali funkciu thread_test ().
- Toto je metóda thread_test (), ktorá berie hodnotu i ako argument, znižuje ju o 1 pri každej iterácii a prechádza zvyškom kódu, kým sa z nej nestane 0. V každej iterácii vytlačí názov práve vykonávaného vlákna a spí na pár sekúnd (čo sa tiež berie ako argument).
- thread1 = tester vlákien (1, "prvé vlákno", 1)
Tu vytvárame vlákno a odovzdávame tri parametre, ktoré sme deklarovali v __init__. Prvý parameter je id vlákna, druhý parameter je názov vlákna a tretí parameter je počítadlo, ktoré určuje, koľkokrát by sa mala cyklus while spustiť.
- thread2.start ()
Na spustenie vykonávania vlákna sa používa metóda start. Funkcia start () interne volá metódu run () vašej triedy.
- thread3.join ()
Metóda join () blokuje vykonávanie iného kódu a počká, kým sa ukončí vlákno, na ktorom sa volala.
Ako už viete, vlákna, ktoré sú v rovnakom procese, majú prístup do pamäte a k údajom tohto procesu. Výsledkom je, že ak sa viac ako jedno vlákno pokúsi zmeniť alebo získať prístup k údajom súčasne, môže dôjsť k vniknutiu chýb.
V nasledujúcej časti uvidíte rôzne druhy komplikácií, ktoré sa môžu prejaviť pri prístupe vlákien k údajom a kritickej sekcii bez kontroly existujúcich prístupových transakcií.
Zablokovanie a podmienky pretekov
Predtým, ako sa dozviete o zablokovaní a podmienkach rasy, bude užitočné pochopiť niekoľko základných definícií týkajúcich sa súbežného programovania:
- Kritická časť
Jedná sa o fragment kódu, ktorý pristupuje alebo upravuje zdieľané premenné a musí byť vykonaný ako atómová transakcia.
- Kontextový prepínač
Jedná sa o proces, ktorý CPU sleduje, aby uložil stav vlákna pred prechodom z jednej úlohy na druhú, aby bolo možné neskôr pokračovať z rovnakého bodu.
Zablokovanie
Deadlocks sú najobávanejšou otázkou, s ktorou sa vývojári stretávajú pri písaní súbežných / viacvláknových aplikácií v pythone. Najlepším spôsobom, ako porozumieť slepej uličke, je použitie klasického príkladu počítačovej vedy, ktorý je známy ako Problém filozofov stolovania.
Vyhlásenie o probléme pre jedálenských filozofov je nasledovné:
Päť filozofov sedí na okrúhlom stole s piatimi taniermi špagiet (druh cestovín) a piatimi vidličkami, ako je to znázornené na obrázku.

Filozof musí v každom okamihu jesť alebo premýšľať.
Filozof musí navyše vziať dve vidlice susediace s ním (tj. Ľavá a pravá vidlička) skôr, ako bude môcť jesť špagety. Problém slepej uličky nastáva, keď všetkých päť filozofov zdvihne svoje pravé vidlice súčasne.
Keďže každý z filozofov má jednu vidličku, všetci budú čakať, kým ostatní zložia vidličku. Výsledkom je, že nikto z nich nebude môcť jesť špagety.
Podobne v súbežnom systéme dôjde k zablokovaniu, keď sa rôzne vlákna alebo procesy (filozofi) pokúsia získať zdieľané systémové prostriedky (vidly) súčasne. Výsledkom je, že žiadny z procesov nemá šancu na vykonanie, pretože čakajú na iný zdroj, ktorý drží nejaký iný proces.
Podmienky pretekov
Podmienkou rasy je nežiaduci stav programu, ktorý nastane, keď systém vykoná dve alebo viac operácií súčasne. Zvážte napríklad toto jednoduché pre cyklus:
i=0; # a global variablefor x in range(100):print(i)i+=1;
Ak vytvoríte n počet vlákien, ktoré spúšťajú tento kód naraz, po ukončení vykonávania programu nemôžete určiť hodnotu i (ktorá je zdieľaná vláknami). Je to tak preto, lebo v skutočnom prostredí s viacerými vláknami sa vlákna môžu prekrývať a hodnota i, ktorá bola vláknom získaná a upravená, sa môže medzi nimi meniť, keď k nim získa prístup nejaké iné vlákno.
Toto sú dve hlavné triedy problémov, ktoré sa môžu vyskytnúť v aplikácii s viacvláknovým alebo distribuovaným pythonom. V nasledujúcej časti sa dozviete, ako prekonať tento problém synchronizáciou vlákien.
Synchronizácia vlákien
Na riešenie problémov v závode, zablokovaní a iných problémov založených na vláknach poskytuje modul vlákien objekt Lock . Myšlienka je taká, že keď vlákno chce prístup k určitému prostriedku, získa pre tento zdroj zámok. Akonáhle vlákno uzamkne konkrétny zdroj, žiadne iné vlákno k nemu nebude mať prístup, kým sa zámok neuvoľní. Vo výsledku budú zmeny zdroja atómové a dôjde k odvráteniu podmienok rasy.
Zámok je primitívum synchronizácie na nízkej úrovni implementované modulom __thread . Zámok môže byť kedykoľvek v jednom z 2 stavov: uzamknutý alebo odomknutý. Podporuje dve metódy:
- získať ()
Keď je stav uzamknutia odomknutý, volanie metódy acqui () zmení stav na zamknutý a vráti sa. Ak je však stav uzamknutý, volanie na získanie () je blokované, kým metódu release () nevyvolá nejaké iné vlákno.
- uvoľnenie ()
Metóda release () sa používa na nastavenie stavu na odomknutý, tj na uvoľnenie zámku. Môže byť volaný akýmkoľvek vláknom, nie nevyhnutne tým, ktoré získalo zámok.
Tu je príklad použitia zámkov vo vašich aplikáciách. Zapnite IDLE a zadajte nasledujúci text:
import threadinglock = 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()
Teraz stlačte kláves F5. Mali by ste vidieť výstup ako tento:
VYSVETLENIE KÓDU
- Tu jednoducho vytvoríte nový zámok zavolaním továrenskej funkcie threading.Lock () . Funkcia Lock () interne vracia inštanciu najefektívnejšej konkrétnej triedy Lock, ktorá je udržiavaná platformou.
- V prvom výroku získate zámok volaním metódy obtain (). Po udelení zámku vytlačíte na konzolu text „zámok získaný“ . Keď sa dokončí vykonávanie celého kódu, ktorý má vlákno spustiť, uvoľníte zámok volaním metódy release ().
Teória je v poriadku, ale ako viete, že zámok skutočne fungoval? Ak sa pozriete na výstup, uvidíte, že každý z tlačových príkazov tlačí súčasne presne jeden riadok. Pripomeňme, že v predchádzajúcom príklade boli výstupy z tlače nešťastné, pretože k metóde print () pristupovalo súčasne viac vlákien. Tu sa funkcia tlače vyvolá až po získaní zámku. Takže výstupy sa zobrazujú po jednom a riadok po riadku.
Okrem zámkov python podporuje aj niektoré ďalšie mechanizmy na synchronizáciu vlákien, ktoré sú uvedené nižšie:
- RLocky
- Semafory
- Podmienky
- Udalosti a
- Prekážky
Globálny tlmočník Lock (a ako s ním zaobchádzať)
Predtým, ako sa dostaneme k podrobnostiam GIL v jazyku python, definujme niekoľko pojmov, ktoré budú užitočné pri porozumení nadchádzajúcej časti:
- Kód viazaný na CPU: označuje akýkoľvek kúsok kódu, ktorý priamo vykoná CPU.
- Kód viazaný na I / O: môže to byť akýkoľvek kód, ktorý pristupuje k súborovému systému cez OS
- CPython: je to referenčná implementácia jazyka Python a možno ho opísať ako tlmočník napísaný v jazykoch C a Python (programovací jazyk).
Čo je to GIL v Pythone?
Globálny tlmočník Lock (GIL) v pythone je zámok procesu alebo mutex používaný pri riešení procesov. Zaisťuje, aby jedno vlákno malo prístup k určitému prostriedku súčasne, a tiež bráni použitiu objektov a bajtových kódov naraz. To prospieva jednovláknovým programom pri zvyšovaní výkonu. GIL v pythone je veľmi jednoduchý a ľahko implementovateľný.
Zámok sa dá použiť na zabezpečenie toho, že k určitému prostriedku má v danom čase prístup iba jedno vlákno.
Jednou z funkcií Pythonu je, že používa globálny zámok na každý proces tlmočníka, čo znamená, že každý proces považuje samotného tlmočníka Pythonu za zdroj.
Predpokladajme napríklad, že ste napísali program v jazyku python, ktorý na vykonávanie operácií CPU a „I / O“ používa dve vlákna. Po spustení tohto programu sa stane toto:
- Tlmočník pythonu vytvorí nový proces a vytvorí vlákna
- Keď vlákno 1 začne bežať, najskôr získa GIL a uzamkne ho.
- Ak chce vlákno-2 vykonať teraz, bude musieť počkať na vydanie GIL, aj keď je voľný ďalší procesor.
- Teraz predpokladajme, že vlákno-1 čaká na vstupno-výstupnú operáciu. V tejto chvíli vydá GIL a vlákno-2 ho získa.
- Po dokončení operácií I / O, ak chce vlákno-1 vykonať teraz, bude musieť opäť počkať na uvoľnenie GIL vláknom-2.
Z tohto dôvodu môže mať k tlmočníkovi kedykoľvek prístup iba jedno vlákno, čo znamená, že v danom okamihu bude existovať iba jedno vlákno vykonávajúce kód python.
To je v prípade jednojadrového procesora v poriadku, pretože na spracovanie vlákien by bolo potrebné časové rozdelenie (pozri prvú časť tohto tutoriálu). Avšak v prípade viacjadrových procesorov bude mať funkcia viazaná na procesor vykonávaná na viacerých vláknach značný vplyv na efektivitu programu, pretože v skutočnosti nebude využívať všetky dostupné jadrá súčasne.
Prečo bol GIL potrebný?
Zberač odpadu CPython používa efektívnu techniku správy pamäte, ktorá sa nazýva počítanie referencií. Funguje to takto: Každý objekt v pythone má počet odkazov, ktorý sa zvýši, keď je priradený k novému názvu premennej alebo pridaný do kontajnera (napríklad n-tice, zoznamy atď.). Rovnako sa zníži počet referencií, keď referencia vypadne z rozsahu alebo keď sa volá príkaz del. Keď počet referencií objektu dosiahne 0, zhromaždí sa odpadky a pridelená pamäť sa uvoľní.
Ale problém je v tom, že premenná počtu referencií je náchylná na rasové podmienky ako každá iná globálna premenná. Na vyriešenie tohto problému sa vývojári jazyka python rozhodli použiť globálny zámok tlmočníka. Druhou možnosťou bolo pridať ku každému objektu zámok, čo by malo za následok zablokovanie a zvýšenú réžiu z volaní acquire () a release ().
Preto je GIL významným obmedzením pre viacvláknové programy v jazyku python, ktoré prevádzkujú ťažké operácie spojené s procesorom (čo ich efektívne robí jednovláknovými). Ak chcete vo svojej aplikácii využiť viac jadier CPU, použite namiesto toho multiprocesorový modul.
Zhrnutie
- Python podporuje 2 moduly pre viacvláknové spracovanie:
- __thread modul: Poskytuje implementáciu na nízkej úrovni pre threading a je zastaraný.
- modul vlákien : Poskytuje implementáciu na vysokej úrovni pre viacvláknové spracovanie a je aktuálnym štandardom.
- Ak chcete vytvoriť vlákno pomocou modulu vlákien, musíte urobiť nasledovné:
- Vytvorte triedu, ktorá rozširuje triedu vlákien .
- Prepísať jeho konštruktor (__init__).
- Prepísať jeho metódu run () .
- Vytvorte objekt tejto triedy.
- Vlákno je možné vykonať volaním metódy start () .
- Pripojiť () metóda môže byť použitá na blokovanie iných vlákien do tohto závitu (na ktorom spojiť sa nazýva) dokončenie spustenia.
- Podmienka rasy sa vyskytne, keď viac vlákien pristupuje alebo upravuje zdieľaný prostriedok súčasne.
- Tomu sa dá vyhnúť synchronizáciou vlákien.
- Python podporuje 6 spôsobov synchronizácie vlákien:
- Zámky
- RLocky
- Semafory
- Podmienky
- Udalosti a
- Prekážky
- Zámky umožňujú vstup do kritickej sekcie iba konkrétnemu vláknu, ktoré získalo zámok.
- Zámok má dve primárne metódy:
- obtain () : Nastaví stav uzamknutia na zamknutý. Ak je vyvolaný na uzamknutom objekte, blokuje sa, kým nebude zdroj voľný.
- release () : Nastaví stav uzamknutia na odomknuté a vráti sa späť. Ak je vyvolaný na odblokovanom objekte, vráti hodnotu false.
- Globálny zámok tlmočníka je mechanizmus, prostredníctvom ktorého je možné súčasne spustiť iba jeden proces tlmočníka CPython.
- Používalo sa to na uľahčenie funkčnosti počítania odkazov zberača odpadu CPythons.
- Ak chcete vytvárať aplikácie v Pythone s náročnými operáciami viazanými na procesor, mali by ste použiť modul s viacerými procesormi.