Многопоточност в Python с Пример: Научете GIL в Python

⚡ Умно обобщение

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

  • 🔘 Нишка срещу процес: Нишката е единица за изпълнение в рамките на процес, а нишките споделят данните на процеса, докато се изпълняват поотделно.
  • ☑️ Два модула: Остарелият модул _thread предлага ниско ниво на извиквания, докато модулът за нишки от по-високо ниво е съвременният стандарт за изграждане на нишки.
  • Създаване на теми: Разширете класа Thread, презапишете __init__ и run(), след което извикайте start() за стартиране и join() за изчакване на завършване.
  • 🧪 Избягвайте състезателни условия: Заключването позволява само на една нишка да влиза в критична секция едновременно, предотвратявайки блокиране и повредени споделени данни.
  • 🛠️ Разберете GIL: Глобалното заключване на интерпретатора позволява изпълнението на една нишка Python байткод наведнъж, така че работата, обвързана с процесора, изисква многопроцесорна обработка.
  • 🤖 Натоварвания на ИИ: Многонишкото четене ускорява входно/изходно обвързаните стъпки на изкуствения интелект, като например зареждане на данни, докато NumPy и PyTorch освобождава GIL за паралелни изчисления.

Многопоточност в Python

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

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

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

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

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

В какво е Multithreading Python?

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

Какво е многопроцесорна обработка?

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

Python Многонишковост срещу многопроцесорност

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

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

  • - Code
  • Данните

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

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

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

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

Python Многопоточност

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

  1. - конец модул и
  2. - резби модул

Въпреки това, в 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, за да стартирате програмата. Ако всичко е направено правилно, това е резултатът, който трябва да видите:

Модулът Thread

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

Модулът Thread

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

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

Модулът Threading

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

Структура на Threading модул

Структура на Threading модул

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

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

Предистория: Класът на нишката

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

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

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

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

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

След като обект на нишка е направен, 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()

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

Предистория: Класът на нишката

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

Предистория: Класът на нишката

  1. Тази част е същата като предишния ни пример. Тук импортирате модулите за време и нишки, които се използват за обработка на изпълнението и закъсненията на Python конци.
  2. В този бит вие създавате клас, наречен threadtester, който наследява или разширява Нишка клас на модула за нишки. Това е един от най-често срещаните начини за създаване на нишки в PythonТрябва обаче да презапишете само конструктора и тичам () метод във вашето приложение. Както можете да видите в примерния код по-горе, __init__ метод (конструктор) е заменен. По същия начин вие също сте заменили тичам () метод. Той съдържа кода, който искате да изпълните в нишка. В този пример сте извикали функцията thread_test().
  3. Това е методът thread_test(), който приема стойността на i като аргумент, го намалява с 1 на всяка итерация и преминава през останалата част от кода, докато i стане 0. Във всяка итерация, той отпечатва името на текущо изпълняващата се нишка и спи за секунди изчакване (което също се приема като аргумент).
  4. thread1 = threadtester(1, “First Thread”, 1) Тук създаваме нишка и предаваме трите параметъра, които сме декларирали в __init__. Първият параметър е идентификаторът на нишката, вторият параметър е името на нишката, а третият параметър е броячът, който определя колко пъти трябва да се изпълнява цикълът while.
  5. thread2.start() Методът start се използва за стартиране на изпълнението на нишка. Вътрешно, функцията start() извиква метода run() на вашия клас.
  6. thread3.join() Методът join() блокира изпълнението на друг код и изчаква, докато нишката, в която е бил извикан, приключи.

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

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

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

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

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

Безизходица

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

Постановката на проблема за трапезните философи е следната:

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

Трапезария Philosophers проблем

Трапезария Philosophers проблем

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

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

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

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

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

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

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

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

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

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

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

Заключването е примитив за синхронизация на ниско ниво, реализиран от _нишка модул. Във всеки един момент заключването може да бъде в едно от две състояния: заключен or отключен. Поддържа два метода:

  1. придобиване(): Когато заключеното състояние е отключено, извикването на метода acquire() ще промени състоянието на заключено и ще върне резултат. Ако обаче състоянието е заключено, извикването на acquire() е блокирано, докато методът release() не бъде извикан от друга нишка.
  2. освобождаване(): Методът 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. Трябва да видите резултат като този:

Syncхронизиране на теми

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

Syncхронизиране на теми

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

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

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

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

Глобално заключване на интерпретатора (и как да се справите с него)

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

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

В какво е GIL Python?

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

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

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

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

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

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

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

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

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

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

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

Въпроси и Отговори

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

Не по подразбиране. PEP 703 въведе опционална свободна нишка за изграждане. Python 3.13, който деактивира GIL, и Python 3.14 го продължава. Стандартните компилации все още доставят GIL, така че повечето програми продължават да работят точно както преди.

Демон нишката работи във фонов режим и не блокира завършването на програмата. Когато останат само демон нишки, Python изключва ги и завършва. Задайте едно с thread.daemon = True преди да извикате start().

Класът Thread не връща директно резултата от run(). Запазете стойността в обекта thread или в споделена опашка, или използвайте concurrent.futures.ThreadPoolExecutor, чийто метод submit() връща Future, който четете с result().

ThreadPoolExecutor е помощник на високо ниво, който управлява набор от работни нишки вместо вас. Вие изпращате задачи с submit() или map(), а той обработва създаването, повторната употреба и почистването на нишките, връщайки обекти Future, които съдържат всеки резултат.

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

Да, главно за стъпки, свързани с входно/изходни операции, като зареждане на данни, извикване на API или четене на файлове. Интензивното обучение е обвързано с процесора, така че библиотеки като NumPy и PyTorch освободете GIL в C код или разчитайте на многопроцесорна обработка за истински паралелизъм.

Да. GitHub Copilot автоматично довършва често срещани шаблони, като например подкласове на Thread, използване на Lock и настройки на ThreadPoolExecutor, от коментар или име на функция. Винаги проверявайте генерирания код за условия на състезание и правилно обработване на заключване, преди да разчитате на него.

Обобщете тази публикация с: