Пример за многопоточност в Python с Global Interpreter Lock (GIL)

Съдържание:

Anonim

Езикът за програмиране на python ви позволява да използвате многопроцесорна обработка или многопоточност. В този урок ще научите как да пишете многонишкови приложения в Python.

Какво е нишка?

Нишката е единица за извличане при едновременно програмиране. Многопоточността е техника, която позволява на процесора да изпълнява много задачи от един процес едновременно. Тези нишки могат да се изпълняват индивидуално, докато споделят ресурсите на процеса си.

Какво е процес?

Процесът е основно програмата в изпълнение. Когато стартирате приложение на вашия компютър (като браузър или текстов редактор), операционната система създава процес.

Какво представлява Multithreading в Python?

Многопоточността в програмирането на Python е добре позната техника, при която множество нишки в даден процес споделят своето пространство с данни с основната нишка, което прави споделянето на информация и комуникацията в нишките лесни и ефективни. Конците са по-леки от процесите. Множество нишки могат да се изпълняват индивидуално, докато споделят ресурсите си от процеса. Целта на многопоточността е да изпълнява едновременно множество задачи и функционални клетки.

Какво е мултипроцесинг?

Мултипроцесингът ви позволява да стартирате множество несвързани процеси едновременно. Тези процеси не споделят своите ресурси и комуникират чрез IPC.

Python Multithreading срещу Multiprocessing

За да разберете процесите и нишките, разгледайте този сценарий: .exe файл на вашия компютър е програма. Когато го отворите, операционната система го зарежда в паметта и процесорът го изпълнява. Екземплярът на програмата, която сега се изпълнява, се нарича процес.

Всеки процес ще има 2 основни компонента:

  • Кодът
  • Информацията

Сега процесът може да съдържа една или повече подчасти, наречени нишки. Това зависи от архитектурата на операционната система,. Можете да мислите за нишка като част от процеса, която може да се изпълни отделно от операционната система.

С други думи, това е поток от инструкции, които могат да се изпълняват независимо от ОС. Нишките в рамките на един процес споделят данните от този процес и са проектирани да работят заедно за улесняване на паралелизма.

В този урок ще научите,

  • Какво е нишка?
  • Какво е процес?
  • Какво е Multithreading?
  • Какво е мултипроцесинг?
  • Python Multithreading срещу Multiprocessing
  • Защо да използваме Multithreading?
  • Python MultiThreading
  • Модулите Thread и Threading
  • Модулът на резбата
  • Модулът за резби
  • Безизходица и състезателни условия
  • Синхронизиране на нишки
  • Какво е GIL?
  • Защо беше необходим GIL?

Защо да използваме Multithreading?

Многопоточността ви позволява да разделите приложението на множество подзадачи и да изпълнявате тези задачи едновременно. Ако използвате правилно многопоточност, скоростта на приложението, производителността и изобразяването могат да бъдат подобрени.

Python MultiThreading

Python поддържа конструкции както за многопроцесорна обработка, така и за многопоточност. В този урок ще се фокусирате предимно върху внедряването на многонишкови приложения с python. Има два основни модула, които могат да се използват за обработка на нишки в Python:

  1. Модулът на конеца и
  2. В резби модула

В python обаче има и нещо, наречено глобално заключване на интерпретатора (GIL). Това не позволява значително увеличаване на производителността и дори може да намали производителността на някои многонишкови приложения. Ще научите всичко за това в предстоящите раздели на този урок.

Модулите Thread и Threading

Двата модула, за които ще научите в този урок, са модулът за нишки и модулът за резби .

Въпреки това, модулът за нишка отдавна е остарял. Започвайки с Python 3, той е определен като остарял и е достъпен само като __thread за обратна съвместимост.

Трябва да използвате модула за резби от по-високо ниво за приложения, които възнамерявате да внедрите. Тук модулът е обхванат само с образователна цел.

Модулът на резбата

Синтаксисът за създаване на нова нишка с помощта на този модул е ​​следният:

thread.start_new_thread(function_name, arguments)

Добре, сега сте разгледали основната теория, за да започнете да кодирате. И така, отворете вашия IDLE или бележник и въведете следното:

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

Запазете файла и натиснете F5, за да стартирате програмата. Ако всичко е направено правилно, това е резултатът, който трябва да видите:

Ще научите повече за състезателните условия и как да се справите с тях в предстоящите раздели

ОБЯСНЕНИЕ НА КОДА

  1. Тези изрази импортират модула за време и нишка, които се използват за обработка на изпълнението и забавянето на нишките на Python.
  2. Тук сте дефинирали функция, наречена thread_test, която ще бъде извикана от метода start_new_thread . Функцията изпълнява цикъл while за четири итерации и отпечатва името на нишката, която я е извикала. След като итерацията приключи, тя отпечатва съобщение, че нишката е завършила изпълнението.
  3. Това е основният раздел на вашата програма. Тук просто извиквате метода start_new_thread с функцията thread_test като аргумент.

    Това ще създаде нова нишка за функцията, която предавате като аргумент, и ще започне да я изпълнява. Имайте предвид, че можете да замените това (thread _ test) с всяка друга функция, която искате да стартирате като нишка.

Модулът за резби

Този модул е ​​изпълнението на нишки на високо ниво в python и де факто стандартът за управление на многонишкови приложения. Той осигурява широка гама от функции в сравнение с модула за резба.

Структура на модула за резби

Ето списък на някои полезни функции, дефинирани в този модул:

Име на функцията Описание
activeCount () Връща броя на нишките, които все още са живи
currentThread () Връща текущия обект от класа Thread.
изброявам () Изброява всички активни обекти на нишка.
isDaemon () Връща true, ако нишката е демон.
isAlive () Връща true, ако нишката е все още жива.
Методи от нишки клас
старт () Стартира активността на нишка. Трябва да се извиква само веднъж за всяка нишка, защото ще изведе грешка по време на изпълнение, ако се извика няколко пъти.
тичам () Този метод обозначава активността на нишка и може да бъде заменен от клас, който разширява класа Thread.
присъединяване() Той блокира изпълнението на друг код, докато нишката, на която е извикан методът join (), бъде прекратена.

Предистория: The Thread Class

Преди да започнете да кодирате многонишкови програми с помощта на модула за резби, е от решаващо значение да разберете за класа Thread. Класът нишка е основният клас, който дефинира шаблона и операциите на нишка в python.

Най-често срещаният начин за създаване на многонишко приложение на python е декларирането на клас, който разширява класа Thread и отменя метода run ().

Класът Thread, обобщено, означава кодова последователност, която работи в отделна нишка на контрол.

Така че, когато пишете многонишко приложение, ще направите следното:

  1. дефинирайте клас, който разширява класа Thread
  2. Заменете конструктора __init__
  3. Замяна на план () метод

След като обектът на нишка бъде направен, методът start () може да се използва за започване на изпълнението на тази дейност, а методът join () може да се използва за блокиране на всички останали кодове, докато текущата дейност приключи.

Сега, нека опитаме да използваме модула за резби, за да приложим предишния ви пример. Отново запалете вашия IDLE и въведете следното:

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

Това ще бъде изходът, когато изпълните горния код:

ОБЯСНЕНИЕ НА КОДА

  1. Тази част е същата като нашия предишен пример. Тук импортирате модула за време и нишка, които се използват за обработка на изпълнението и закъсненията на нишките на Python.
  2. В този бит създавате клас, наречен threadtester, който наследява или разширява класа Thread на модула за резба. Това е един от най-често срещаните начини за създаване на нишки в python. Трябва обаче да замените конструктора и метода run () във вашето приложение. Както можете да видите в горната примерна програма, методът __init__ (конструктор) е заменен .

    По същия начин сте заменили и метода run () . Той съдържа кода, който искате да изпълните вътре в нишка. В този пример сте извикали функцията thread_test ().

  3. Това е методът thread_test (), който приема стойността на i като аргумент, намалява я с 1 при всяка итерация и прелиства останалата част от кода, докато i стане 0. Във всяка итерация отпечатва името на изпълняваната в момента нишка и спи за секунди за изчакване (което също се приема като аргумент).
  4. thread1 = threadtester (1, "Първа нишка", 1)

    Тук създаваме нишка и предаваме трите параметъра, които сме декларирали в __init__. Първият параметър е идентификаторът на нишката, вторият параметър е името на нишката, а третият параметър е броячът, който определя колко пъти трябва да се изпълнява цикълът while.

  5. thread2.start ()

    Методът старт се използва за стартиране на изпълнението на нишка. Вътрешно функцията start () извиква метода run () на вашия клас.

  6. thread3.join ()

    Методът join () блокира изпълнението на друг код и изчаква, докато нишката, на която е наречен, завърши.

Както вече знаете, нишките, които са в един и същ процес, имат достъп до паметта и данните от този процес. В резултат на това, ако повече от една нишка се опитват да променят или да получат достъп до данните едновременно, може да се промъкнат грешки.

В следващия раздел ще видите различните видове усложнения, които могат да се появят, когато нишките имат достъп до данни и критична секция, без да проверявате за съществуващи транзакции за достъп.

Безизходица и състезателни условия

Преди да научите за безизходиците и състезателните условия, ще бъде полезно да разберете няколко основни определения, свързани с едновременното програмиране:

  • Критичен раздел

    Това е фрагмент от код, който осъществява достъп или модифицира споделени променливи и трябва да се изпълнява като атомна транзакция.

  • Контекстен превключвател

    Това е процесът, който процесорът следва, за да съхранява състоянието на нишка, преди да се промени от една задача на друга, за да може да бъде възобновена от същата точка по-късно.

Безизходица

Безизходиците са най-страшният проблем, пред който са изправени разработчиците, когато пишат едновременни / многонишкови приложения в python. Най-добрият начин за разбиране на задънените пътища е използването на класическия пример за компютърни науки, известен като Проблемът на философите за хранене

Изложението на проблема за философите за хранене е следното:

Петима философи са седнали на кръгла маса с пет чинии спагети (вид паста) и пет вилици, както е показано на диаграмата.

Проблем на философите за хранене

Във всеки един момент един философ трябва или да яде, или да мисли.

Освен това, един философ трябва да вземе двете съседни вилици (т.е. лявата и дясната вилица), преди да може да изяде спагетите. Проблемът с безизходицата възниква, когато и петимата философи вземат едновременно десните си разклонения.

Тъй като всеки от философите има по една вилица, всички те ще чакат останалите да сложат вилицата си. В резултат никой от тях няма да може да яде спагети.

По същия начин, в едновременна система, настъпва блокировка, когато различни нишки или процеси (философи) се опитват да придобият споделените системни ресурси (разклонения) едновременно. В резултат на това нито един от процесите няма шанс да се изпълни, тъй като те очакват друг ресурс, задържан от някой друг процес.

Състезателни условия

Състезателно състояние е нежелано състояние на програма, което възниква, когато системата извършва две или повече операции едновременно. Например, помислете за този прост цикъл for:

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

Ако създадете n броя нишки, които изпълняват този код наведнъж, не можете да определите стойността на i (която се споделя от нишките), когато програмата завърши изпълнението. Това е така, защото в реална среда с много нишки нишките могат да се припокриват и стойността на i, която е била извлечена и модифицирана от нишка, може да се променя между тях, когато някой друг поток има достъп до нея.

Това са двата основни класа проблеми, които могат да възникнат в многонишково или разпределено приложение на python. В следващия раздел ще научите как да преодолеете този проблем чрез синхронизиране на нишки.

Синхронизиране на нишки

За справяне с условията на състезанието, мъртвата блокировка и други проблеми, базирани на нишки, модулът за резби предоставя обекта Lock . Идеята е, че когато нишката иска достъп до определен ресурс, тя придобива заключване за този ресурс. След като нишка заключи определен ресурс, никоя друга нишка няма достъп до него, докато заключването не бъде освободено. В резултат на това промените в ресурса ще бъдат атомни и условията на състезанието ще бъдат предотвратени.

Заключването е примитив за синхронизация на ниско ниво, реализиран от модула __thread . По всяко време ключалката може да бъде в едно от 2 състояния: заключена или отключена. Той поддържа два метода:

  1. придобивам()

    Когато състоянието на заключване е отключено, извикването на метода придобиване () ще промени състоянието на заключено и ще се върне. Ако обаче състоянието е заключено, повикването за придобиване () се блокира, докато методът release () бъде извикан от друга нишка.

  2. освобождаване ()

    Методът освобождаване () се използва за задаване на отключено състояние, т.е. за освобождаване на заключване. Може да се извика от всяка нишка, не е задължително тази, която е придобила ключалката.

Ето пример за използване на ключалки във вашите приложения. Задействайте вашия IDLE и напишете следното:

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

Сега натиснете F5. Трябва да видите изход като този:

ОБЯСНЕНИЕ НА КОДА

  1. Тук просто създавате нова ключалка, като извикате фабричната функция Threading.Lock () . Вътрешно Lock () връща екземпляр на най-ефективния конкретен клас Lock, който се поддържа от платформата.
  2. В първото изявление вие ​​придобивате заключването, като извикате метода за придобиване (). Когато заключването е предоставено, отпечатвате „заключена придобивка“ на конзолата. След като целият код, който искате да изпълнява нишката, приключи, освобождавате заключването, като извикате метода release ().

Теорията е наред, но откъде знаете, че ключалката наистина е работила? Ако погледнете резултата, ще видите, че всеки от инструкциите за печат отпечатва точно по един ред наведнъж. Спомнете си, че в по-ранен пример изходите от print са случайни, тъй като множество нишки имат достъп до метода print () едновременно. Тук функцията за печат се извиква само след придобиване на заключването. И така, изходите се показват един по един и ред по ред.

Освен ключалки, python поддържа и някои други механизми за обработка на синхронизация на нишки, както е изброено по-долу:

  1. RLocks
  2. Семафори
  3. Условия
  4. Събития и
  5. Бариери

Global Interpreter Lock (и как да се справим с него)

Преди да влезем в подробностите за GIL на python, нека дефинираме няколко термина, които ще бъдат полезни при разбирането на предстоящия раздел:

  1. Код, свързан с процесора: това се отнася до всяка част от кода, която ще бъде изпълнена директно от процесора.
  2. Код, свързан с I / O: това може да бъде всеки код, който осъществява достъп до файловата система чрез „ОС“
  3. CPython: това е референтната реализация на Python и може да бъде описана като интерпретатор, написан на C и Python (език за програмиране).

Какво е GIL в Python?

Global Interpreter Lock (GIL) в python е заключване на процес или мютекс, използван при работа с процесите. Той гарантира, че една нишка може да има достъп до определен ресурс в даден момент и също така предотвратява използването на обекти и байт кодове наведнъж. Това се възползва от еднонишковите програми за повишаване на производителността. GIL в python е много лесен и лесен за изпълнение.

Заключване може да се използва, за да се гарантира, че само една нишка има достъп до определен ресурс в даден момент.

Една от характеристиките на Python е, че използва глобално заключване на всеки процес на интерпретатор, което означава, че всеки процес третира самия интерпретатор на python като ресурс.

Да предположим например, че сте написали програма на python, която използва две нишки, за да изпълнява както CPU, така и „I / O“ операции. Когато изпълнявате тази програма, се случва следното:

  1. Интерпретаторът на python създава нов процес и създава нишките
  2. Когато thread-1 започне да работи, първо ще придобие GIL и ще го заключи.
  3. Ако thread-2 иска да се изпълни сега, ще трябва да изчака освобождаването на GIL, дори ако друг процесор е свободен.
  4. Сега, да предположим, че thread-1 чака операция I / O. По това време той ще освободи GIL и нишката-2 ще го придобие.
  5. След завършване на I / O операциите, ако thread-1 иска да се изпълни сега, отново ще трябва да изчака GIL да бъде освободен от thread-2.

Поради това само една нишка може да има достъп до интерпретатора по всяко време, което означава, че ще има само една нишка, изпълняваща python код в даден момент от времето.

Това е добре в едноядрен процесор, защото би използвал нарязване на времето (вижте първия раздел на този урок) за обработка на нишките. Въпреки това, в случай на многоядрени процесори, свързана с процесора функция, изпълняваща се в множество нишки, ще има значително въздействие върху ефективността на програмата, тъй като всъщност няма да използва всички налични ядра едновременно.

Защо беше необходим GIL?

Събирачът на боклук CPython използва ефективна техника за управление на паметта, известна като преброяване на референции. Ето как работи: Всеки обект в python има референтен брой, който се увеличава, когато е присвоен на ново име на променлива или добавен към контейнер (като кортежи, списъци и т.н.). По същия начин броят на препратките се намалява, когато препратката излезе извън обхвата или когато се извика операторът del. Когато броят на референциите на даден обект достигне 0, той се събира и боклукът се освобождава.

Но проблемът е, че референтната променлива на броя е склонна към състезателни условия като всяка друга глобална променлива. За да разрешат този проблем, разработчиците на python решиха да използват глобалното заключване на интерпретатора. Другата опция беше да добавите заключване към всеки обект, което би довело до блокировки и увеличаване на режийните разходи от повикванията ((и освобождаване)).

Следователно GIL е значително ограничение за многонишковите програми на python, изпълняващи тежки операции, свързани с процесора (ефективно ги прави еднонишкови). Ако искате да използвате множество ядра на процесора във вашето приложение, вместо това използвайте многопроцесорния модул.

Обобщение

  • Python поддържа 2 модула за многопоточност:
    1. __thread модул: Той осигурява изпълнение на ниско ниво за резби и е остарял.
    2. модул за резби : Той осигурява изпълнение на високо ниво за многопоточност и е настоящият стандарт.
  • За да създадете нишка с помощта на модула за резби, трябва да направите следното:
    1. Създайте клас, който разширява класа Thread .
    2. Заменете конструктора му (__init__).
    3. Заменете метода му run () .
    4. Създайте обект от този клас.
  • Нишка може да бъде изпълнена чрез извикване на метода start () .
  • Методът join () може да се използва за блокиране на други нишки, докато тази нишка (тази, на която беше извикано присъединяването) завърши изпълнението.
  • Състезателно състояние възниква, когато множество нишки имат достъп или променят споделен ресурс едновременно.
  • Може да се избегне чрез синхронизиране на нишки.
  • Python поддържа 6 начина за синхронизиране на нишки:
    1. Брави
    2. RLocks
    3. Семафори
    4. Условия
    5. Събития и
    6. Бариери
  • Бравите позволяват само на определена нишка, която е придобила ключалката, да влезе в критичния участък.
  • Заключването има 2 основни метода:
    1. придобиване () : Задава заключено състояние на заключено. Ако се извика заключен обект, той се блокира, докато ресурсът не се освободи.
    2. release () : Задава заключено състояние на отключено и се връща. Ако бъде извикан за отключен обект, той връща false.
  • Глобалното заключване на интерпретатора е механизъм, чрез който само 1 CPython процес на интерпретатор може да се изпълни наведнъж.
  • Той беше използван за улесняване на функцията за преброяване на референции на колектора за боклук на CPythons.
  • За да правите приложения на Python с тежки операции, свързани с процесора, трябва да използвате многопроцесорния модул.