Многопоточност в Python с Пример: Научете GIL в Python
Какво е нишка?
Нишката е единица за изпълнение при едновременно програмиране. Многопоточността е техника, която позволява на процесора да изпълнява много задачи на един процес едновременно. Тези нишки могат да се изпълняват поотделно, докато споделят ресурсите на процеса.
Какво е процес?
Процесът е основно програмата в изпълнение. Когато стартирате приложение на вашия компютър (като браузър или текстов редактор), операционната система създава a процес.
В какво е Multithreading Python?
Многопоточност в Python програмирането е добре позната техника, при която множество нишки в процес споделят своето пространство от данни с основната нишка, което прави споделянето на информация и комуникацията в нишките лесни и ефективни. Нишките са по-леки от процесите. Множеството нишки могат да се изпълняват поотделно, докато споделят своите ресурси на процеса. Целта на многопоточността е да изпълнява множество задачи и функционални клетки едновременно.
Какво е многопроцесорна обработка?
многопроцесорна ви позволява да изпълнявате множество несвързани процеси едновременно. Тези процеси не споделят своите ресурси и комуникират чрез IPC.
Python Многонишковост срещу многопроцесорност
За да разберете процесите и нишките, разгледайте този сценарий: .exe файл на вашия компютър е програма. Когато го отворите, ОС го зарежда в паметта и процесорът го изпълнява. Екземплярът на програмата, който сега се изпълнява, се нарича процес.
Всеки процес ще има 2 основни компонента:
- Кодексът
- Данните
Сега един процес може да съдържа една или повече подчасти, наречени конци. Това зависи от архитектурата на операционната система. Можете да мислите за нишката като част от процеса, която може да се изпълнява отделно от операционната система.
С други думи, това е поток от инструкции, който може да се изпълнява независимо от операционната система. Нишките в рамките на един процес споделят данните от този процес и са проектирани да работят заедно за улесняване на паралелизма.
Защо да използвате Multithreading?
Многопоточността ви позволява да разбиете приложение на множество подзадачи и да изпълнявате тези задачи едновременно. Ако използвате многонишковостта правилно, скоростта, производителността и изобразяването на вашето приложение могат да бъдат подобрени.
Python Многопоточност
Python поддържа конструкции както за многопроцесорна, така и за многонишкова обработка. В този урок ще се съсредоточите основно върху внедряването многонишков приложения с python. Има два основни модула, които могат да се използват за обработка на нишки Python:
- - конец модул и
- - резби модул
В Python обаче има и нещо, наречено глобално заключване на интерпретатора (GIL). Това не позволява много повишаване на производителността и може дори намаляване на производителността на някои многонишкови приложения. Ще научите всичко за това в следващите раздели на този урок.
Модулите Thread и Threading
Двата модула, за които ще научите в този урок, са модул за резба и модул за резби.
Модулът за нишки обаче отдавна е отхвърлен. Започвайки с Python 3, той е определен като остарял и е достъпен само като __нишка за обратна съвместимост.
Трябва да използвате по-високото ниво резби модул за приложения, които възнамерявате да внедрите. Модулът за нишка е разгледан тук само за образователни цели.
Модулът Thread
Синтаксисът за създаване на нова нишка с помощта на този модул е както следва:
thread.start_new_thread(function_name, arguments)
Добре, вече покрихте основната теория, за да започнете да кодирате. Така че, отворете своя IDLE или бележник и въведете следното:
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))
Запазете файла и натиснете F5, за да стартирате програмата. Ако всичко е направено правилно, това е резултатът, който трябва да видите:
Ще научите повече за условията на състезанието и как да се справите с тях в следващите раздели
ОБЯСНЕНИЕ НА КОДА
- Тези изрази импортират модула за време и нишка, които се използват за обработка на изпълнението и забавянето на Python конци.
- Тук сте дефинирали функция, наречена нишка_тест, който ще бъде извикан от стартиране_нова_нишка метод. Функцията изпълнява цикъл while за четири итерации и отпечатва името на нишката, която я е извикала. След като итерацията приключи, тя отпечатва съобщение, че нишката е завършила изпълнението.
- Това е основният раздел на вашата програма. Тук просто се обаждате на стартиране_нова_нишка метод с нишка_тест функция като аргумент. Това ще създаде нова нишка за функцията, която подавате като аргумент, и ще започне да я изпълнява. Имайте предвид, че можете да замените това (нишка_test) с всяка друга функция, която искате да стартирате като нишка.
Модулът Threading
Този модул е внедряването на високо ниво на нишката в Python и де факто стандартът за управление на многонишкови приложения. Той предоставя широк набор от функции в сравнение с модула за нишки.
Ето списък на някои полезни функции, дефинирани в този модул:
Име на функция | Descriptйон |
---|---|
activeCount() | Връща броя на Нишка обекти, които все още са живи |
currentThread() | Връща текущия обект на класа Thread. |
изброявам() | Изброява всички активни обекти Thread. |
isDaemon() | Връща true, ако нишката е демон. |
isAlive() | Връща true, ако нишката е все още жива. |
Методи на класове на нишки | |
начало() | Стартира активността на нишка. Трябва да се извика само веднъж за всяка нишка, защото ще изведе грешка по време на изпълнение, ако бъде извикана многократно. |
тичам () | Този метод обозначава активността на нишка и може да бъде заменен от клас, който разширява класа Thread. |
присъединяване() | Той блокира изпълнението на друг код, докато нишката, на която е извикан методът join(), не бъде прекратена. |
Предистория: Класът на нишката
Преди да започнете да кодирате многонишкови програми с помощта на модула за нишки, е изключително важно да разберете за класа Thread. Класът нишка е основният клас, който дефинира шаблона и операциите на нишка в python.
Най-често срещаният начин за създаване на многонишково приложение на Python е да се декларира клас, който разширява класа Thread и замества неговия метод run().
Класът Thread, накратко, означава кодова последователност, която се изпълнява отделно конец на контрол.
Така че, когато пишете многонишково приложение, ще направите следното:
- дефинирайте клас, който разширява класа Thread
- Замени __init__ конструктор
- Замени тичам () метод
След като обект на нишка е направен, the начало() методът може да се използва за започване на изпълнението на тази дейност и присъединяване() метод може да се използва за блокиране на целия друг код, докато текущата дейност приключи.
Сега, нека опитаме да използваме модула за нишки, за да реализираме предишния ви пример. Отново запалете своя IDLE и въведете следното:
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()
Това ще бъде изходът, когато изпълните горния код:
ОБЯСНЕНИЕ НА КОДА
- Тази част е същата като нашия предишен пример. Тук импортирате модула за време и нишка, които се използват за обработка на изпълнението и закъсненията на Python конци.
- В този бит вие създавате клас, наречен threadtester, който наследява или разширява Нишка клас на модула за нишки. Това е един от най-често срещаните начини за създаване на нишки в Python. Трябва обаче да замените само конструктора и тичам () метод във вашето приложение. Както можете да видите в примерния код по-горе, __init__ метод (конструктор) е заменен. По същия начин вие също сте заменили тичам () метод. Той съдържа кода, който искате да изпълните в нишка. В този пример сте извикали функцията thread_test().
- Това е методът thread_test(), който приема стойността на i като аргумент, намалява го с 1 при всяка итерация и преминава през останалата част от кода, докато i стане 0. При всяка итерация той отпечатва името на текущо изпълняваната нишка и заспива за секунди на изчакване (което също се приема като аргумент ).
- thread1 = threadtester(1, “First Thread”, 1) Тук създаваме нишка и предаваме трите параметъра, които сме декларирали в __init__. Първият параметър е идентификаторът на нишката, вторият параметър е името на нишката, а третият параметър е броячът, който определя колко пъти трябва да се изпълнява цикълът while.
- thread2.start()T методът start се използва за стартиране на изпълнението на нишка. Вътрешно функцията start() извиква метода run() на вашия клас.
- thread3.join() Методът join() блокира изпълнението на друг код и изчаква, докато нишката, в която е бил извикан, приключи.
Както вече знаете, нишките, които са в един и същ процес, имат достъп до паметта и данните на този процес. В резултат на това, ако повече от една нишка се опита да промени или осъществи достъп до данните едновременно, може да се появят грешки.
В следващия раздел ще видите различните видове усложнения, които могат да се появят, когато нишките имат достъп до данни и критична секция, без да проверяват за съществуващи транзакции за достъп.
Безизходици и условия на състезание
Преди да научите за задънените блокировки и условията на състезание, ще бъде полезно да разберете няколко основни дефиниции, свързани с едновременното програмиране:
- Критична секция Това е фрагмент от код, който осъществява достъп или променя споделени променливи и трябва да се изпълнява като атомна транзакция.
- Превключване на контекст Това е процесът, който процесорът следва, за да съхранява състоянието на нишката, преди да премине от една задача към друга, така че да може да бъде възобновена от същата точка по-късно.
Безизходица
Безизходица са най-страшният проблем, с който разработчиците се сблъскват, когато пишат едновременни/многонишкови приложения в python. Най-добрият начин да разберете задънените блокировки е като използвате класическия пример за компютърни науки, известен като Трапезария Philosophers проблем.
Постановката на проблема за трапезните философи е следната:
Петима философи са седнали на кръгла маса с пет чинии спагети (вид паста) и пет вилици, както е показано на диаграмата.
Във всеки един момент един философ трябва или да яде, или да мисли.
Освен това, философът трябва да вземе двете съседни вилици (т.е. лявата и дясната вилица), преди да може да изяде спагетите. Проблемът с безизходицата възниква, когато и петимата философи вдигнат десните си вилици едновременно.
Тъй като всеки от философите има една вилица, всички те ще чакат другите да оставят вилицата си. В резултат никой от тях няма да може да яде спагети.
По същия начин, в едновременна система, блокиране възниква, когато различни нишки или процеси (философи) се опитват да придобият споделените системни ресурси (форкове) едновременно. В резултат на това нито един от процесите няма шанс да се изпълни, тъй като те чакат друг ресурс, държан от друг процес.
Състезателни условия
Състоянието на състезание е нежелано състояние на програма, което възниква, когато системата изпълнява две или повече операции едновременно. Например, разгледайте този прост цикъл for:
i=0; # a global variable for x in range(100): print(i) i+=1;
Ако създадете n брой нишки, които изпълняват този код наведнъж, не можете да определите стойността на i (която се споделя от нишките), когато програмата завърши изпълнението. Това е така, защото в реална многонишкова среда нишките могат да се припокриват и стойността на i, която е извлечена и модифицирана от нишка, може да се промени между тях, когато друга нишка има достъп до нея.
Това са двата основни класа проблеми, които могат да възникнат в многонишково или разпределено приложение на Python. В следващия раздел ще научите как да преодолеете този проблем чрез синхронизиране на нишки.
Syncхронизиране на нишки
За да се справи с условията на състезание, блокирания и други проблеми, базирани на нишки, модулът за нишки предоставя Заключвам обект. Идеята е, че когато една нишка иска достъп до определен ресурс, тя получава заключване за този ресурс. След като дадена нишка заключи определен ресурс, никоя друга нишка няма достъп до нея, докато заключването не бъде освободено. В резултат на това промените в ресурса ще бъдат атомарни и условията на състезание ще бъдат избегнати.
Заключването е примитив за синхронизация на ниско ниво, реализиран от __нишка модул. Във всеки един момент една ключалка може да бъде в едно от 2 състояния: заключен or отключен. Поддържа два метода:
- придобивам ()Когато състоянието на заключване е отключено, извикването на метода придобиване() ще промени състоянието на заключено и ще се върне. Въпреки това, ако състоянието е заключено, извикването за придобиване() се блокира, докато методът release() не бъде извикан от друга нишка.
- освобождаване()Методът release() се използва за задаване на състоянието на unlocked, т.е. за освобождаване на заключване. Може да бъде извикан от всяка нишка, не непременно тази, която е придобила ключалката.
Ето пример за използване на ключалки във вашите приложения. Запалете своя IDLE и въведете следното:
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()
Сега натиснете F5. Трябва да видите резултат като този:
ОБЯСНЕНИЕ НА КОДА
- Тук вие просто създавате нова ключалка, като извикате threading.Lock() фабрична функция. Вътрешно Lock() връща екземпляр на най-ефективния конкретен клас Lock, който се поддържа от платформата.
- В първия оператор получавате заключването чрез извикване на метода придобиване(). Когато заключването е предоставено, вие отпечатвате „заключване придобито“ към конзолата. След като целият код, който искате нишката да изпълнява, завърши изпълнението, вие освобождавате заключването, като извиквате метода release().
Теорията е добра, но как да разберете, че ключалката наистина е работила? Ако погледнете изхода, ще видите, че всеки от операторите за печат отпечатва точно един ред наведнъж. Спомнете си, че в по-ранен пример изходите от print бяха случайни, тъй като множество нишки имаха достъп до метода print() едновременно. Тук функцията за печат се извиква само след получаване на заключването. Така че резултатите се показват един по един и ред по ред.
Освен ключалки, python поддържа и някои други механизми за обработка на синхронизирането на нишки, както е изброено по-долу:
- RLocks
- Semaphores
- Условия
- Събития и
- Бариери
Глобално заключване на интерпретатора (и как да се справите с него)
Преди да навлезем в детайлите на GIL на python, нека дефинираме няколко термина, които ще бъдат полезни за разбирането на предстоящия раздел:
- Код, свързан с процесора: това се отнася за всяка част от кода, която ще бъде директно изпълнена от процесора.
- I/O-свързан код: това може да бъде всеки код, който осъществява достъп до файловата система през операционната система
- CPython: това е справката изпълнение of Python и може да бъде описан като интерпретатор, написан на C и Python (език за програмиране).
В какво е GIL Python?
Глобално заключване на интерпретатора (GIL) в python е заключване на процес или мютекс, използван при работа с процесите. Той гарантира, че една нишка може да има достъп до определен ресурс в даден момент и също така предотвратява използването на обекти и байт кодове наведнъж. Това е от полза за еднонишковите програми за увеличаване на производителността. GIL в Python е много прост и лесен за изпълнение.
Може да се използва заключване, за да се гарантира, че само една нишка има достъп до определен ресурс в даден момент.
Една от характеристиките на Python е, че използва глобално заключване за всеки процес на интерпретатор, което означава, че всеки процес третира самия интерпретатор на python като ресурс.
Да предположим например, че сте написали програма на Python, която използва две нишки за извършване както на CPU, така и на „I/O“ операции. Когато изпълните тази програма, се случва следното:
- Интерпретаторът на Python създава нов процес и ражда нишките
- Когато нишката-1 започне да се изпълнява, тя първо ще придобие GIL и ще го заключи.
- Ако нишка-2 иска да се изпълни сега, тя ще трябва да изчака GIL да бъде освободен, дори ако друг процесор е свободен.
- Сега да предположим, че нишка-1 чака I/O операция. По това време той ще освободи GIL и thread-2 ще го придобие.
- След завършване на I/O операциите, ако нишка-1 иска да се изпълни сега, тя отново ще трябва да изчака GIL да бъде освободен от нишка-2.
Поради това само една нишка може да има достъп до интерпретатора по всяко време, което означава, че ще има само една нишка, изпълняваща код на Python в даден момент от време.
Това е добре в едноядрен процесор, защото би използвал времево нарязване (вижте първия раздел на този урок) за обработка на нишките. Въпреки това, в случай на многоядрени процесори, обвързана с процесора функция, изпълняваща се на множество нишки, ще има значително въздействие върху ефективността на програмата, тъй като тя всъщност няма да използва всички налични ядра едновременно.
Защо беше необходим GIL?
СPython събирачът на боклук използва ефективна техника за управление на паметта, известна като броене на референтни данни. Ето как работи: Всеки обект в Python има брой препратки, който се увеличава, когато се присвои на ново име на променлива или се добави към контейнер (като кортежи, списъци и т.н.). По същия начин броят на препратките се намалява, когато препратката излезе извън обхвата или когато се извика командата del. Когато броят на препратките на даден обект достигне 0, той се събира за боклук и разпределената памет се освобождава.
Но проблемът е, че променливата за броя на препратките е склонна към условия на състезание, както всяка друга глобална променлива. За да разрешат този проблем, разработчиците на Python решиха да използват глобалното заключване на интерпретатора. Другата опция беше да се добави заключване към всеки обект, което би довело до блокиране и увеличаване на режийните разходи от извиквания на accept() и release().
Следователно GIL е значително ограничение за многонишкови програми на python, изпълняващи тежки CPU-обвързани операции (ефективно ги прави еднонишкови). Ако искате да използвате няколко CPU ядра във вашето приложение, използвайте многопроцесорна обработка модул вместо това.
Oбобщение
- Python поддържа 2 модула за многопоточност:
- __нишка модул: Осигурява внедряване на ниско ниво за нишки и е остарял.
- модул за резби: Осигурява внедряване на високо ниво за многопоточност и е текущият стандарт.
- За да създадете нишка с помощта на модула за нишки, трябва да направите следното:
- Създайте клас, който разширява Нишка клас.
- Замени неговия конструктор (__init__).
- Замени своя тичам () метод.
- Създайте обект от този клас.
- Една нишка може да бъде изпълнена чрез извикване на начало() метод.
- - присъединяване() метод може да се използва за блокиране на други нишки, докато тази нишка (тази, на която е извикано присъединяването) завърши изпълнението.
- Условие на състезание възниква, когато множество нишки осъществяват достъп или променят споделен ресурс едновременно.
- Може да се избегне чрез Syncхронизиране на нишки.
- Python поддържа 6 начина за синхронизиране на нишки:
- Брави
- RLocks
- Semaphores
- Условия
- Събития и
- Бариери
- Заключванията позволяват само определена нишка, която е придобила заключването, да влезе в критичната секция.
- Заключването има 2 основни метода:
- придобивам (): Задава състоянието на заключване на заключена. Ако се извика на заключен обект, той блокира, докато ресурсът се освободи.
- освобождаване(): Задава състоянието на заключване на отключен и се връща. Ако се извика на отключен обект, той връща false.
- Глобалното заключване на интерпретатора е механизъм, чрез който само 1 CPython процесът на интерпретатор може да се изпълнява наведнъж.
- Той беше използван за улесняване на функционалността за преброяване на справки на CPythonбоклукчия на s.
- За да направите Python приложения с тежки обвързани с процесора операции, трябва да използвате мултипроцесорния модул.