Мова програмування python дозволяє використовувати багатопроцесорну обробку або багатопотоковість. У цьому посібнику ви дізнаєтеся, як писати багатопотокові програми на Python.
Що таке нитка?
Потік - це одиниця вимірювання при одночасному програмуванні. Багатопотоковість - це техніка, яка дозволяє центральному процесору виконувати багато завдань одного процесу одночасно. Ці потоки можуть виконуватися індивідуально під час спільного використання своїх ресурсів процесу.
Що таке процес?
Процес - це в основному програма, що виконується. Коли ви запускаєте програму на своєму комп’ютері (наприклад, браузер або текстовий редактор), операційна система створює процес.
Що таке багатопоточність у Python?
Багатопотоковість у програмуванні на Python - це добре відома техніка, коли декілька потоків у процесі обмінюються своїм простором даних з основним потоком, що робить обмін інформацією та зв'язок у потоках простим та ефективним. Нитки легші за процеси. Багато потоків можуть виконуватися індивідуально під час спільного використання своїх ресурсів процесу. Метою багатопоточності є одночасне виконання декількох завдань та функціональних комірок.
Що таке багатопроцесорність?
Багатопроцесорна обробка дозволяє одночасно запускати кілька не пов’язаних між собою процесів. Ці процеси не діляться своїми ресурсами та не спілкуються через IPC.
Багатопотоковість Python проти багатопроцесорної обробки
Щоб зрозуміти процеси та потоки, розглянемо такий сценарій: Файл .exe на вашому комп’ютері - це програма. Коли ви відкриваєте його, ОС завантажує його в пам’ять, а центральний процесор виконує. Екземпляр запущеної програми називається процесом.
Кожен процес буде мати 2 основні компоненти:
- Код
- Дані
Тепер процес може містити одну або кілька підчастин, які називаються потоками. Це залежить від архітектури ОС,. Ви можете розглядати потік як розділ процесу, який операційна система може виконувати окремо.
Іншими словами, це потік інструкцій, які ОС може запускати самостійно. Потоки в рамках одного процесу обмінюються даними цього процесу і призначені для спільної роботи для полегшення паралелізму.
У цьому підручнику ви дізнаєтесь,
- Що таке нитка?
- Що таке процес?
- Що таке багатопоточність?
- Що таке багатопроцесорність?
- Багатопотоковість Python проти багатопроцесорної обробки
- Навіщо використовувати багатопотоковість?
- Python MultiThreading
- Модулі Thread і Threading
- Модуль потоку
- Модуль різьблення
- Тупикові ситуації та умови перегонів
- Синхронізація потоків
- Що таке GIL?
- Навіщо потрібен GIL?
Навіщо використовувати багатопотоковість?
Багатопотоковість дозволяє розбити додаток на кілька підзавдань і запускати ці завдання одночасно. Якщо ви правильно використовуєте багатопотоковість, можна покращити швидкість, продуктивність та візуалізацію програми.
Python MultiThreading
Python підтримує конструкції як для багатопроцесорної, так і для багатопотокової роботи. У цьому посібнику ви в першу чергу зосередитесь на реалізації багатопоточних програм за допомогою python. Є два основних модулі, які можна використовувати для обробки потоків у Python:
- Нитка модуль,
- різьблень модуль
Однак у 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, щоб запустити програму. Якщо все було зроблено правильно, ось результат, який ви повинні побачити:
Ви дізнаєтесь більше про умови перегонів та як з ними поводитись у наступних розділах
ПОЯСНЕННЯ КОДУ
- Ці оператори імпортують модуль часу та потоку, які використовуються для обробки виконання та затримки потоків Python.
- Тут ви визначили функцію з назвою thread_test, яку буде викликати метод start_new_thread . Функція запускає цикл while протягом чотирьох ітерацій і друкує ім'я потоку, який його викликав. Після завершення ітерації друкується повідомлення про те, що потік закінчив виконання.
- Це основний розділ вашої програми. Тут, ви просто викличте start_new_thread метод з thread_test функції в якості аргументу.
Це створить новий потік для функції, яку ви передаєте як аргумент, і почне її виконувати. Зверніть увагу, що ви можете замінити це (thread _ test) будь-якою іншою функцією, яку ви хочете запустити як нитку.
Модуль різьблення
Цей модуль - це високорівнева реалізація потоків у python та фактичний стандарт управління багатопотоковими додатками. Він забезпечує широкий спектр функцій у порівнянні з потоковим модулем.
Ось список деяких корисних функцій, визначених у цьому модулі:
Назва функції | Опис |
activeCount () | Повертає кількість теми об'єктів , які все ще живі |
currentThread () | Повертає поточний об'єкт класу Thread. |
перерахувати () | Список усіх активних об’єктів Thread. |
isDaemon () | Повертає true, якщо потоком є демон. |
живий() | Повертає true, якщо потік ще живий. |
Методи Thread Class | |
start () | Починає діяльність потоку. Його потрібно викликати лише один раз для кожного потоку, оскільки він викличе помилку виконання, якщо буде викликаний кілька разів. |
запустити () | Цей метод позначає активність потоку і може бути замінений класом, який розширює клас Thread. |
приєднатися () | Він блокує виконання іншого коду, поки потік, для якого був викликаний метод join (), не припиняється. |
Передісторія: Клас ниток
Перш ніж розпочати кодування багатопотокових програм за допомогою потокового модуля, важливо зрозуміти клас Thread. Клас потоку - це основний клас, який визначає шаблон та операції потоку в python.
Найпоширенішим способом створення багатопотокової програми python є оголошення класу, який розширює клас Thread і замінює метод run ().
Клас Thread, у підсумку, означає послідовність кодів, яка працює в окремому потоці управління.
Отже, під час написання багатопотокової програми ви зробите наступне:
- визначити клас, який розширює клас Thread
- Замінити конструктор __init__
- Замінити метод run ()
Після створення об’єкта потоку метод 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()
Це буде результат, коли ви виконаєте наведений вище код:
ПОЯСНЕННЯ КОДУ
- Ця частина така ж, як і наш попередній приклад. Тут ви імпортуєте модуль часу та потоку, які використовуються для обробки виконання та затримок потоків Python.
- У цьому біті ви створюєте клас, який називається threadtester, який успадковує або розширює клас Thread модуля потоків. Це один із найпоширеніших способів створення потоків у python. Однак ви повинні замінити конструктор та метод run () у вашому додатку. Як ви можете бачити у наведеному вище зразку коду, метод __init__ (конструктор) було замінено.
Подібним чином ви також перевизначили метод run () . Він містить код, який ви хочете виконати всередині потоку. У цьому прикладі ви викликали функцію thread_test ().
- Це метод thread_test (), який приймає значення i як аргумент, зменшує його на 1 на кожній ітерації та перебирає решту коду, поки i не стає 0. У кожній ітерації він друкує ім'я поточного потоку, що виконується і спить протягом секунд очікування (що також приймається як аргумент).
- thread1 = threadtester (1, "Перша нитка", 1)
Тут ми створюємо потік і передаємо три параметри, які ми оголосили в __init__. Перший параметр - це ідентифікатор потоку, другий параметр - ім’я потоку, а третій параметр - лічильник, який визначає, скільки разів повинен виконуватися цикл while.
- thread2.start ()
Метод запуску використовується для запуску виконання потоку. Внутрішньо функція start () викликає метод run () вашого класу.
- 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 станів: заблокований або розблокований. Він підтримує два методи:
- придбати ()
Коли стан блокування розблоковано, виклик методу придбання () змінить стан на заблокований і повернеться. Однак, якщо стан заблоковано, виклик для набуття () блокується, доки метод звільнення () не буде викликаний якимсь іншим потоком.
- випуск ()
Метод release () використовується для встановлення розблокованого стану, тобто для звільнення блокування. Її може викликати будь-яка нитка, не обов’язково та, яка придбала замок.
Ось приклад використання блокування у ваших програмах. Запустіть свій 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. Ви повинні побачити такий вивід:
ПОЯСНЕННЯ КОДУ
- Тут ви просто створюєте новий замок, викликаючи заводську функцію threadading.Lock () . Внутрішньо Lock () повертає екземпляр найефективнішого конкретного класу Lock, який підтримується платформою.
- У першому операторі ви отримуєте блокування, викликаючи метод accept (). Коли замок надано, ви друкуєте "замок, придбаний" на консолі. Як тільки весь код, для якого ви хочете, щоб запустився потік, закінчив виконання, ви звільняєте блокування, викликаючи метод release ().
Теорія чудова, але звідки ви знаєте, що замок справді працював? Якщо ви подивитесь на вихідні дані, то побачите, що кожен з операторів друку друкує рівно по одному рядку за раз. Нагадаємо, що в попередньому прикладі виходи з друку були випадковими, оскільки кілька методів одночасно отримували доступ до методу print (). Тут функція друку викликається лише після отримання блокування. Отже, результати виводяться по черзі та по черзі.
Окрім блокування, python також підтримує деякі інші механізми для обробки потокової синхронізації, як зазначено нижче:
- RLocks
- Семафори
- Умови
- Події, і
- Бар'єри
Global Interpreter Lock (і як з цим боротися)
Перш ніж вдаватися до подробиць GIL python, давайте визначимо кілька термінів, які будуть корисні для розуміння майбутнього розділу:
- Код, пов'язаний з процесором: це стосується будь-якого фрагмента коду, який буде безпосередньо виконуватися процесором.
- Код, пов’язаний з введенням / виведенням: це може бути будь-який код, який отримує доступ до файлової системи через ОС
- CPython: це посилальна реалізація Python і може бути описана як інтерпретатор, написаний на мовах C та Python (мова програмування).
Що таке GIL у Python?
Global Interpreter Lock (GIL) у python - це блокування процесу або мьютекс, що використовується під час роботи з процесами. Це гарантує, що один потік може одночасно отримувати доступ до певного ресурсу, а також запобігає використанню об'єктів і байт-кодів одночасно. Це сприяє підвищенню продуктивності однопотокових програм. GIL в python дуже простий і легкий у реалізації.
Блокування можна використовувати, щоб переконатися, що лише один потік має доступ до певного ресурсу в даний момент часу.
Однією з особливостей Python є те, що він використовує глобальний замок для кожного процесу інтерпретатора, а це означає, що кожен процес трактує самого інтерпретатора python як ресурс.
Наприклад, припустимо, ви написали програму на пітоні, яка використовує два потоки для виконання як процесора, так і операцій вводу-виводу. Коли ви виконуєте цю програму, відбувається ось що:
- Інтерпретатор python створює новий процес і породжує потоки
- Коли потік-1 почне працювати, він спочатку отримає GIL і заблокує його.
- Якщо потік-2 хоче виконати зараз, йому доведеться дочекатися випуску GIL, навіть якщо інший процесор вільний.
- Тепер, припустимо, потік-1 чекає операції вводу-виводу. В цей час він випустить GIL, а потік-2 придбає його.
- Після завершення операцій вводу-виводу, якщо потік-1 хоче виконати зараз, йому знову доведеться дочекатися випуску GIL за допомогою потоку-2.
Завдяки цьому лише один потік може отримати доступ до інтерпретатора в будь-який час, що означає, що в даний момент часу буде лише один потік, що виконує код python.
Це нормально для одноядерного процесора, оскільки для обробки потоків він би використовував зрізування часу (див. Перший розділ цього посібника). Однак у випадку з багатоядерними процесорами функція, пов'язана з процесором, яка виконується в декількох потоках, матиме значний вплив на ефективність програми, оскільки вона фактично не буде використовувати всі доступні ядра одночасно.
Навіщо потрібен GIL?
Збирач сміття CPython використовує ефективний метод управління пам’яттю, відомий як підрахунок посилань. Ось як це працює: Кожен об’єкт у python має кількість посилань, яка збільшується, коли вона присвоюється новому імені змінної або додається до контейнера (наприклад, кортежі, списки тощо). Подібним чином кількість посилань зменшується, коли посилання виходить за межі обсягу або коли викликається оператор del. Коли кількість посилань на об'єкт досягає 0, він збирає сміття, і виділена пам'ять звільняється.
Але проблема полягає в тому, що змінна лічильника посилань схильна до расових умов, як і будь-яка інша глобальна змінна. Щоб вирішити цю проблему, розробники python вирішили використовувати глобальну блокування інтерпретатора. Інший варіант полягав у додаванні блокування до кожного об’єкта, що призвело б до тупикових ситуацій та збільшення накладних витрат від викликів придбання () та звільнення ().
Тому GIL є суттєвим обмеженням для багатопотокових програм на пітоні, що виконують важкі операції, пов'язані з процесором (ефективно роблячи їх однопоточними). Якщо ви хочете використовувати декілька ядер ЦП у своїй програмі, використовуйте натомість багатопроцесорний модуль.
Резюме
- Python підтримує 2 модулі для багатопоточності:
- Модуль __thread : Він забезпечує низькорівневу реалізацію потоків і застарів.
- потоковий модуль : Він забезпечує високорівневу реалізацію для багатопоточності і є чинним стандартом.
- Щоб створити потік за допомогою потокового модуля, потрібно виконати наступне:
- Створіть клас, який розширює клас Thread .
- Замінити його конструктор (__init__).
- Перевизначте метод run () .
- Створіть об’єкт цього класу.
- Потік можна виконати, викликавши метод start () .
- Метод join () можна використовувати для блокування інших потоків, поки цей потік (той, на якому було викликано join) не завершить виконання.
- Умова перегонів виникає, коли кілька потоків одночасно отримують доступ або змінюють спільний ресурс.
- Цього можна уникнути, синхронізуючи потоки.
- Python підтримує 6 способів синхронізації потоків:
- Замки
- RLocks
- Семафори
- Умови
- Події, і
- Бар'єри
- Замки дозволяють лише певній нитці, яка придбала замок, потрапляти в критичну секцію.
- Блокування має 2 основних методи:
- придбати () : Встановлює заблокований стан блокування. Якщо викликати заблокований об’єкт, він блокується, доки ресурс не стане вільним.
- release () : Встановлює розблокований стан блокування та повертається. Якщо викликати незаблокований об'єкт, він повертає значення false.
- Глобальна блокування інтерпретатора - це механізм, за допомогою якого одночасно може виконуватися лише 1 процес інтерпретатора CPython.
- Він був використаний для полегшення функцій підрахунку посилань збирача сміття CPythons.
- Щоб створювати програми Python з важкими операціями, пов'язаними з процесором, слід використовувати багатопроцесорний модуль.