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

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

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

  • 🔘 Thread vs process: Нишката е единица за изпълнение в рамките на процес, а нишките споделят данните на процеса, докато се изпълняват поотделно.
  • ☑️ Two modules: Остарелият модул _thread предлага ниско ниво на извиквания, докато модулът за нишки от по-високо ниво е съвременният стандарт за изграждане на нишки.
  • Създаване на теми: Extend the Thread class, override __init__ and run(), then call start() to launch and join() to wait for completion.
  • 🧪 Avoid race conditions: Заключването позволява само на една нишка да влиза в критична секция едновременно, предотвратявайки блокиране и повредени споделени данни.
  • 🛠️ Understand the GIL: Глобалното заключване на интерпретатора позволява изпълнението на една нишка Python байткод наведнъж, така че работата, обвързана с процесора, изисква многопроцесорна обработка.
  • 🤖 Натоварвания на ИИ: Multithreading speeds input/output-bound AI steps like data loading, while NumPy and PyTorch освобождава GIL за паралелни изчисления.

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

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

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

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

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

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

В какво е Multithreading Python?

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

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

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

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

To understand processes and threads, consider this scenario: An .exe file on your computer is a program. When you open it, the OS loads it into memory, and the CPU executes it. The instance of the program that is now running is called the process.

Every process has two fundamental components:

  • - Code
  • Данните

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

In other words, it is a stream of instructions that can be run independently by the OS. Threads within a single process share the data of that process and are designed to work together to facilitate parallelism.

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

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

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

Python supports constructs for both multiprocessing and multithreading. In this tutorial, you will primarily focus on implementing многонишков приложения с 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

You will learn more about race conditions and how to handle them in the upcoming sections.

Модулът Thread

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

  1. These statements import the time and thread module, which are used to handle the execution and delaying of the 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 application is to declare a class that extends the Thread class and overrides its run() method.

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

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

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

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

Now, let us try using the threading module to implement your previous example. Again, fire up your 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. This part is the same as our previous example. Here, you import the time and thread module, which are used to handle the execution and delays of the 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() The start method is used to start the execution of a thread. Internally, the start() function calls the run() method of your class.
  6. thread3.join() Методът join() блокира изпълнението на друг код и изчаква, докато нишката, в която е бил извикан, приключи.

As you already know, the threads that are in the same process have access to the memory and data of that process. As a result, if more than one thread tries to change or access the data simultaneously, errors may creep in.

In the next section, you will see the different kinds of complications that can show up when threads access data and the critical section without checking for existing access transactions.

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

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

  • Критична секция: Това е фрагмент от код, който осъществява достъп до или променя споделени променливи и трябва да се извърши като атомарна транзакция.
  • Превключване на контекста: It is the process that a CPU follows to store the state of a thread before changing from one task to another so that it can be resumed from the same point later.

Безизходица

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

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

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

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

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

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

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

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

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

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

A race condition is an unwanted state of a program that occurs when a system performs two or more operations simultaneously. For example, consider this simple for loop:

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

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

These are the two main classes of problems that can occur in a multithreaded or distributed Python application. In the next section, you will learn how to overcome this problem by synchronizing threads.

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

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

Заключването е примитив за синхронизация на ниско ниво, реализиран от _нишка module. At any given time, a lock can be in one of two states: заключен or отключен. Поддържа два метода:

  1. придобиване(): When the lock state is unlocked, calling the acquire() method will change the state to locked and return. However, if the state is locked, the call to acquire() is blocked until the release() method is called by some other thread.
  2. 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. Трябва да видите резултат като този:

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

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

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

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

The theory is fine, but how do you know that the lock really worked? If you look at the output, you will see that each of the print statements is printing exactly one line at a time. Recall that, in an earlier example, the outputs from print were haphazard because multiple threads were accessing the print() method at the same time. Here, the print function is called only after the lock is acquired. So, the outputs are displayed one at a time and line by line.

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

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

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

Преди да навлезем в подробностите Python’s GIL, let us define a few terms that will be useful in understanding the upcoming section:

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

В какво е GIL Python?

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

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

Една от характеристиките на Python is that it uses a global lock on each interpreter process, which means that every process treats the 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 код в даден момент от времето.

This is alright in a single-core processor because it would be using time slicing (see the first section of this tutorial) to handle the threads. However, in the case of multi-core processors, a CPU-bound function executing on multiple threads will have a considerable impact on the program’s efficiency since it will not actually be using all the available cores at the same time.

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

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

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

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

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

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

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

A daemon thread runs in the background and does not block the program from exiting. When only daemon threads remain, Python изключва ги и завършва. Задайте едно с thread.daemon = True преди да извикате start().

The Thread class does not return the run() result directly. Store the value on the thread object or a shared queue, or use concurrent.futures.ThreadPoolExecutor, whose submit() method returns a Future you read with result().

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

You can create hundreds or thousands of threads, but the GIL lets only one execute Python bytecode at any instant. They still overlap during I/O waits, so many concurrent threads help I/O-bound programs far more than CPU-bound ones.

Yes, mainly for I/O-bound steps such as loading data, calling APIs, or reading files. Heavy training is CPU-bound, so libraries like NumPy and PyTorch release the GIL in C code or rely on multiprocessing for true parallelism.

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

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