Багатопотоковість в Python з прикладом: Вивчайте GIL в Python
⚡ Розумний підсумок
Багатопотоковість в Python запускає кілька потоків в одному процесі, тому вони спільно використовують пам'ять і працюють одночасно. Модуль потоків створює та керує цими потоками, тоді як глобальне блокування інтерпретатора обмежує справжній паралелізм, що робить цей метод найкращим для завдань, пов'язаних з вводом/виводом.
Команда Python Мова програмування дозволяє використовувати багатопроцесорну або багатопотокову обробку. У цьому посібнику ви дізнаєтеся, як писати багатопотокові програми в Python.
Що таке нитка?
Потік — це одиниця виконання в паралельному програмуванні. Багатопоточність — це техніка, яка дозволяє процесору виконувати багато завдань одного процесу одночасно. Ці потоки можуть виконуватися окремо, спільно використовуючи ресурси процесу.
Що таке процес?
Процес – це, по суті, програма, що виконується. Коли ви запускаєте програму на своєму комп’ютері (наприклад, браузер або текстовий редактор), операційна система створює процесу.
Що таке багатопотоковість Python?
Багатопотоковість в Python Програмування — це добре відомий метод, за якого кілька потоків у процесі спільно використовують свій простір даних з головним потоком, що спрощує та спрощує обмін інформацією та комунікацію всередині потоків. Потоки легші за процеси. Кілька потоків можуть виконуватися окремо, спільно використовуючи ресурси процесу. Мета багатопоточності — виконувати кілька завдань і функцій одночасно.
Що таке багатопроцесорність?
Багатопроцесорна дозволяє запускати декілька непов’язаних процесів одночасно. Ці процеси не ділять свої ресурси та спілкуються через IPC.
Python Багатопотоковість проти багатопроцесорності
Щоб зрозуміти процеси та потоки, розглянемо такий сценарій: файл .exe на вашому комп'ютері – це програма. Коли ви його відкриваєте, ОС завантажує його в пам'ять, а процесор виконує його. Екземпляр програми, який зараз працює, називається процесом.
Кожен процес має два фундаментальні компоненти:
- Команда Code
- Дані
Тепер процес може містити одну або кілька підчастин, які називаються нитки. Це залежить від архітектури ОС. Ви можете уявити потік як частину процесу, яка може виконуватися окремо операційною системою.
Іншими словами, це потік інструкцій, які можуть виконуватися ОС незалежно. Потоки в межах одного процесу спільно використовують дані цього процесу та призначені для спільної роботи для забезпечення паралелізму.
Навіщо використовувати багатопотоковість?
Багатопотоковість дозволяє розбити програму на кілька підзавдань і виконувати ці завдання одночасно. Якщо ви належним чином використовуєте багатопотоковість, швидкість вашої програми, продуктивність і візуалізація можуть бути покращені.
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, щоб запустити програму. Якщо все зроблено правильно, ви повинні побачити ось результат:
Ви дізнаєтеся більше про умови перегонів та про те, як з ними впоратися, у наступних розділах.
ПОЯСНЕННЯ КОДУ
- Ці оператори імпортують модуль time та thread, які використовуються для обробки виконання та затримки Python нитки.
- Тут ви визначили функцію під назвою thread_test, який буде називатися почати_новий_потік метод. Функція виконує цикл while протягом чотирьох ітерацій та виводить ім'я потоку, який його викликав. Після завершення ітерації вона виводить повідомлення про те, що потік завершив виконання.
- Це основний розділ вашої програми. Тут ви просто телефонуєте почати_новий_потік метод за допомогою thread_test функцію як аргумент. Це створить новий потік для функції, яку ви передаєте як аргумент, і почне її виконання. Зверніть увагу, що ви можете замінити це (thread_test) будь-якою іншою функцією, яку ви хочете запустити як потік.
Модуль Threading
Цей модуль є високорівневою реалізацією потоків у Python і фактичний стандарт для керування багатопотоковими програмами. Він надає широкий спектр функцій порівняно з модулем thread.
Структура модуля Threading
Ось список деяких корисних функцій, визначених у цьому модулі:
| Назва функції | Опис |
|---|---|
| activeCount() | Повертає кількість Нитка предмети, які досі живі. |
| currentThread() | Повертає поточний об’єкт класу Thread. |
| перерахувати () | Перелічує всі активні об’єкти Thread. |
| isDaemon() | Повертає true, якщо потік є демоном. |
| живий() | Повертає true, якщо потік ще живий. |
| Методи класу потоків | |
| start () | Починає діяльність потоку. Він має бути викликаний лише один раз для кожного потоку, тому що він викличе помилку під час виконання, якщо викликати кілька разів. |
| запустити () | Цей метод позначає активність потоку та може бути перевизначений класом, який розширює клас Thread. |
| приєднатися () | Він блокує виконання іншого коду, доки потік, у якому було викликано метод join(), не буде завершено. |
Backstory: The Thread Class
Перш ніж почати кодувати багатопотокові програми за допомогою модуля потоків, важливо зрозуміти клас Thread. Клас thread – це основний клас, який визначає шаблон та операції потоку в Python.
Найпоширеніший спосіб створення багатопотокового Python застосування полягає в оголошення класу, який розширює клас Thread та перевизначає його метод run().
Загалом, клас Thread означає послідовність коду, яка виконується окремо нитка контролю.
Отже, під час написання багатопоточної програми ви зробите наступне:
- визначити клас, який розширює клас Thread
- Переосмислити __init__ конструктор
- Переосмислити запустити () метод
Після створення об’єкта потоку, start () метод може бути використаний для початку виконання цієї дії, і приєднатися () метод можна використовувати для блокування всього іншого коду до завершення поточної дії.
Тепер спробуємо використати модуль потоків для реалізації вашого попереднього прикладу. Знову ж таки, запустіть ваш 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()
Це буде результат, коли ви виконаєте наведений вище код:
ПОЯСНЕННЯ КОДУ
- Ця частина така ж, як і наш попередній приклад. Тут ви імпортуєте модулі time та thread, які використовуються для обробки виконання та затримок Python нитки.
- У цьому фрагменті ви створюєте клас під назвою threadtester, який успадковує або розширює Нитка клас модуля потокової обробки. Це один із найпоширеніших способів створення потоків у PythonОднак, вам слід лише перевизначити конструктор та запустити () у вашій програмі. Як ви можете бачити в наведеному вище прикладі коду, __init__ метод (конструктор) перевизначено. Подібним чином ви також перевизначили запустити () метод. Він містить код, який ви хочете виконати всередині потоку. У цьому прикладі ви викликали функцію thread_test().
- Це метод thread_test(), який приймає значення i як аргумент, зменшує його на 1 на кожній ітерації та проходить решту коду, доки i не стане 0. На кожній ітерації він друкує назву потоку, що виконується, та переходить у режим очікування на секунди (що також береться як аргумент).
- thread1 = threadtester(1, “First Thread”, 1) Тут ми створюємо потік і передаємо три параметри, які ми оголосили в __init__. Перший параметр – це ідентифікатор потоку, другий параметр – це ім’я потоку, а третій параметр – лічильник, який визначає, скільки разів має виконуватися цикл while.
- thread2.start() Метод start використовується для запуску виконання потоку. Внутрішньо функція start() викликає метод run() вашого класу.
- thread3.join() Метод join() блокує виконання іншого коду та чекає, поки не закінчиться потік, у якому він був викликаний.
Як ви вже знаєте, потоки, що знаходяться в одному процесі, мають доступ до пам'яті та даних цього процесу. Як наслідок, якщо більше одного потоку намагається змінити або отримати доступ до даних одночасно, можуть виникнути помилки.
У наступному розділі ви побачите різні види ускладнень, які можуть виникнути, коли потоки отримують доступ до даних та критичної секції без перевірки наявних транзакцій доступу.
Тупикові ситуації та умови перегонів
Перш ніж вивчати глухі блокування та умови гонки, буде корисно зрозуміти кілька основних визначень, пов'язаних з паралельним програмуванням:
- Критичний розділ: Це фрагмент коду, який отримує доступ до спільних змінних або змінює їх, і його необхідно виконувати як атомарну транзакцію.
- Перемикач контексту: Це процес, який виконує процесор для збереження стану потоку перед переходом від одного завдання до іншого, щоб його можна було відновити з тієї ж точки пізніше.
Тупикові ситуації
Тупикові ситуації є найстрашнішою проблемою, з якою стикаються розробники під час написання паралельних/багатопотокових додатків у PythonНайкращий спосіб зрозуміти глухі блокування – це використовувати класичний приклад задачі з інформатики, відомий як Обідній Philoпроблема sophers.
Постановка проблеми для філософів-обідань така:
П'ять філософів сидять за круглим столом, на якому лежить п'ять тарілок спагеті (вид пасти) та п'ять виделок, як показано на діаграмі.
Обідній Philoпроблема sophers
У будь-який момент часу філософ повинен або їсти, або думати.
Крім того, філософ повинен взяти дві виделки, які поряд з ним (тобто ліву і праву виделки), перш ніж він зможе з'їсти спагетті. Проблема тупика виникає, коли всі п'ять філософів одночасно беруть свої праві вилки.
Оскільки кожен із філософів має одну виделку, усі вони чекатимуть, поки інші покладуть свою виделку. В результаті жоден з них не зможе їсти спагетті.
Так само в паралельній системі взаємоблокування виникає, коли різні потоки або процеси (філософи) намагаються отримати спільні системні ресурси (форки) одночасно. У результаті жоден із процесів не має шансу виконатися, оскільки вони чекають іншого ресурсу, який утримує інший процес.
Умови гонки
Умова змагання — це небажаний стан програми, який виникає, коли система виконує дві або більше операцій одночасно. Наприклад, розглянемо цей простий цикл for:
i=0; # a global variable for x in range(100): print(i) i+=1;
Якщо ви створюєте n З огляду на кількість потоків, які одночасно виконують цей код, ви не можете визначити значення i (яке спільно використовується потоками) після завершення виконання програми. Це пояснюється тим, що в реальному середовищі багатопотокової обробки потоки можуть перекриватися, а значення i, яке було отримано та змінено потоком, може змінюватися між ними, коли до нього звертається якийсь інший потік.
Це два основні класи проблем, які можуть виникнути в багатопотоковій або розподіленій системі. Python застосунок. У наступному розділі ви дізнаєтеся, як подолати цю проблему шляхом синхронізації потоків.
Syncхронізація ниток
Для вирішення умов змагання, взаємоблокувань та інших проблем, пов’язаних із потоками, модуль потоків надає Lock об'єкт. Ідея полягає в тому, що коли потік хоче отримати доступ до певного ресурсу, він отримує блокування для цього ресурсу. Як тільки потік блокує певний ресурс, жоден інший потік не може отримати до нього доступ, доки блокування не буде знято. У результаті зміни в ресурсі будуть атомарними, а умови перегонів будуть уникнені.
Блокування — це низькорівневий примітив синхронізації, реалізований за допомогою _thread модуль. У будь-який момент часу блокування може перебувати в одному з двох станів: замкнений or розблоковано. Він підтримує два методи:
- отримати(): Коли стан блокування розблоковано, виклик методу acquire() змінить стан на locked та поверне результат. Однак, якщо стан заблоковано, виклик acquire() блокується, доки метод 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, який підтримується платформою.
- У першому операторі ви отримуєте блокування, викликаючи метод accept(). Коли блокування надано, ви друкуєте «блокування отримано» до консолі. Після завершення виконання всього коду, який ви хочете запустити в потокі, ви знімаєте блокування, викликаючи метод release().
Теорія чудова, але як дізнатися, що блокування справді спрацювало? Якщо ви подивитеся на вивід, то побачите, що кожен з операторів print друкує рівно один рядок за раз. Нагадаємо, що в попередньому прикладі вивід функції print був безладним, оскільки кілька потоків одночасно зверталися до методу print(). Тут функція print викликається лише після отримання блокування. Отже, вивід відображається по одному рядку за рядком.
Окрім замків, Python також підтримує деякі інші механізми для обробки синхронізації потоків, як зазначено нижче:
- RLocks
- Semaphores
- Conditions
- Події, і
- Бар'єри
Глобальне блокування інтерпретатора (і як з цим боротися)
Перш ніж заглиблюватися в деталі PythonЩодо GIL, давайте визначимо кілька термінів, які будуть корисними для розуміння наступного розділу:
- Код, пов'язаний з процесором: це стосується будь-якого фрагмента коду, який буде виконуватися безпосередньо процесором.
- Код, пов'язаний з вводом/виводом: це може бути будь-який код, який отримує доступ до файлової системи через ОС.
- CPython: це посилання реалізація of Python і може бути описаний як інтерпретатор, написаний на C і Python (мова програмування).
Що таке GIL Python?
Глобальне блокування інтерпретатора (GIL) in Python — це блокування процесу або м'ютекс, що використовується під час роботи з процесами. Він гарантує, що один потік може одночасно отримувати доступ до певного ресурсу, а також запобігає одночасному використанню об'єктів та байт-кодів. Це вигідно для однопотокових програм, оскільки збільшує продуктивність. GIL у Python дуже простий та легкий у виконанні.
Блокування можна використовувати, щоб переконатися, що лише один потік має доступ до певного ресурсу в певний момент часу.
Одна з особливостей Python полягає в тому, що він використовує глобальне блокування для кожного процесу інтерпретатора, що означає, що кожен процес обробляє Python сам інтерпретатор як ресурс.
Наприклад, припустимо, що ви написали Python програма, яка використовує два потоки для виконання операцій як процесора, так і операцій «вводу/виводу». Під час виконання цієї програми відбувається ось що:
- Команда Python Інтерпретатор створює новий процес і породжує потоки.
- Коли потік-1 починає працювати, він спочатку отримає GIL і заблокує його.
- Якщо потік-2 хоче виконати зараз, йому доведеться дочекатися випуску GIL, навіть якщо інший процесор вільний.
- Тепер припустімо, що потік-1 очікує на операцію введення-виведення. У цей час він випустить GIL, і потік-2 отримає його.
- Після завершення операцій введення-виведення, якщо потік-1 хоче виконати зараз, йому знову доведеться чекати, поки GIL буде звільнено потоком-2.
Через це лише один потік може отримати доступ до інтерпретатора одночасно, а це означає, що виконуватиметься лише один потік. Python код у заданий момент часу.
Це нормально для одноядерного процесора, оскільки для обробки потоків використовувалося б розподіл часу (див. перший розділ цього посібника). Однак у випадку багатоядерних процесорів функція, пов'язана з процесором, яка виконується на кількох потоках, матиме значний вплив на ефективність програми, оскільки вона фактично не використовуватиме всі доступні ядра одночасно.
Навіщо потрібен був GIL?
СPython Збирач сміття використовує ефективний метод керування пам'яттю, відомий як підрахунок посилань. Ось як це працює: кожен об'єкт у Python має лічильник посилань, який збільшується, коли його присвоюють новій змінній або додають до контейнера (наприклад, кортежів, списків тощо). Аналогічно, лічильник посилань зменшується, коли посилання виходить за межі області видимості або коли викликається оператор del. Коли лічильник посилань об'єкта досягає 0, він збирається як сміття, а виділена пам'ять звільняється.
Але проблема полягає в тому, що змінна лічильника посилань, як і будь-яка інша глобальна змінна, схильна до умов змагання. Щоб вирішити цю проблему, розробники Python вирішили використовувати глобальне блокування інтерпретатора. Іншим варіантом було додати блокування до кожного об'єкта, що призвело б до взаємоблокувань та збільшення накладних витрат від викликів acquire() та release().
Таким чином, GIL є суттєвим обмеженням для багатопотокових Python програми, що виконують операції з високим навантаженням на процесор (що фактично робить їх однопоточними). Якщо ви хочете використовувати кілька ядер процесора у своїй програмі, використовуйте багатопроцесорний модуль замість цього.









