У цій інструкції ви освоїте застосування вбудованого модуля для роботи з потоками Python, щоб дослідити можливості багатопоточності в Python.
Розпочинаючи з основних понять процесів та потоків, ви з’ясуєте, як працює багатопотоковість в Python, одночасно розуміючи концепцію паралельності та конкурентності. Далі ви дізнаєтеся, як ініціювати та виконувати один або декілька потоків у Python, використовуючи вбудований модуль потоків.
Почнемо.
Відмінності між процесами та потоками
Що являє собою процес?
Процес – це будь-яка інстанція програми, призначена для запуску.
Це може бути різноманітне: від скрипта Python або веб-браузера, такого як Chrome, до програми для відеоконференцій. Запустивши диспетчер задач на вашому комп’ютері та перейшовши до “Продуктивність” –> “ЦП”, ви зможете побачити перелік процесів і потоків, що зараз функціонують на ядрах вашого процесора.
Основи процесів та потоків
Кожен процес має свою власну область пам’яті, де зберігаються код та дані, що належать цьому процесу.
Процес може включати один або більше потоків. Потік – це мінімальна послідовність інструкцій, що може бути виконана операційною системою, і він відображає потік виконання.
Кожен потік має свій стек та набір регістрів, але не виділену пам’ять. Усі потоки, пов’язані з конкретним процесом, мають спільний доступ до його даних. Таким чином, дані та пам’ять використовуються спільно всіма потоками цього процесу.
На процесорі з N ядрами N процесів можуть функціонувати паралельно в один і той самий момент часу. Однак, два потоки одного процесу не можуть виконуватися паралельно, але можуть виконуватися одночасно. У наступному розділі ми розглянемо концепції конкурентності та паралелізму.
Опираючись на набуті знання, підсумуємо відмінності між процесом та потоком.
Характеристика | Процес | Потік |
Пам’ять | Виділена пам’ять | Спільна пам’ять |
Режим виконання | Паралельний, конкурентний | Конкурентний; але не паралельний |
Виконання обробляється | Операційною системою | CPython інтерпретатором |
Багатопотоковість у Python
Глобальне блокування інтерпретатора (GIL) в Python забезпечує, що тільки один потік може отримати блокування і виконуватися в будь-який конкретний момент часу. Усі потоки мають отримати це блокування, щоб мати змогу виконуватись. Це гарантує, що лише один потік буде у виконанні – в будь-який момент часу – і запобігає справжній одночасній багатопоточності.
Наприклад, якщо маємо два потоки t1 і t2 в одному процесі. Оскільки потоки використовують ті самі дані, коли t1 читає певне значення k, t2 може змінити це значення k. Це може призвести до взаємних блокувань та небажаних результатів. Але тільки один з потоків може отримати блокування і працювати одночасно. Тому GIL також гарантує потокобезпеку.
Отже, як ми досягаємо можливостей багатопоточності в Python? Щоб це зрозуміти, розглянемо поняття паралелізму та конкурентності.
Паралелізм проти конкурентності: загальний огляд
Уявімо процесор з кількома ядрами. На малюнку нижче процесор має чотири ядра. Це означає, що ми здатні виконувати чотири різні операції паралельно в будь-який момент часу.
Якщо є чотири процеси, то кожен з них може працювати незалежно і одночасно на кожному з чотирьох ядер. Припустимо, що кожен процес має два потоки.
Для розуміння, як працює потокова передача, перейдемо від багатоядерної архітектури процесора до одноядерної. Як уже згадувалося, лише один потік може бути активним у певний момент виконання; але ядро процесора може переключатися між потоками.
Наприклад, потоки, пов’язані з вводом-виводом, часто очікують на операції введення-виведення: читання введених користувачем даних, читання з бази даних та файлові операції. Протягом цього періоду очікування він може звільнити блокування, щоб інший потік міг виконуватись. Час очікування може бути також звичайною операцією, такою як сон протягом n секунд.
Підсумовуючи: під час операцій очікування потік звільняє блокування, дозволяючи ядру процесора перемкнутися на інший потік. Попередній потік відновлює виконання після завершення періоду очікування. Цей процес, коли ядро процесора одночасно переключається між потоками, сприяє багатопоточності. ✅
Якщо ви хочете реалізувати паралелізм на рівні процесу у вашій програмі, розгляньте можливість використання багатопроцесорності.
Модуль потоків Python: перші кроки
Python постачається з модулем обробки потоків, який можна імпортувати у ваш сценарій Python.
import threading
Для створення об’єкта потоку в Python, використовується конструктор Thread: threading.Thread(…)
. Загальний синтаксис, який достатній для більшості реалізацій потоків:
threading.Thread(target=...,args=...)
де,
target
– це іменований аргумент, що вказує на функцію Python.args
– кортеж аргументів, які приймає цільова функція.
Для виконання прикладів коду в цьому посібнику, вам знадобиться Python 3.x. Завантажте код і слідуйте інструкціям.
Як ініціювати та запустити потоки в Python
Створимо потік, який виконує цільову функцію.
Цільова функція – some_func
.
import threading
import time
def some_func():
print("Виконується some_func...")
time.sleep(2)
print("Завершено виконання some_func.")
thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())
Розглянемо, що робить наведений вище код:
- Імпортує модулі
threading
іtime
. - Функція
some_func
містить інструкціїprint()
та операцію затримки на дві секунди:time.sleep(n)
змушує функцію “заснути” на n секунд. - Далі ми ініціалізуємо потік
thread_1
, ціллю якого єsome_func
.threading.Thread(target=…)
створює об’єкт потоку. - Зверніть увагу: використовуйте назву функції, а не її виклик; застосовуйте
some_func
, а неsome_func()
. - Створення об’єкта потоку не запускає потік; викликайте метод
start()
для об’єкта потоку. - Для отримання кількості активних потоків ми використовуємо функцію
active_count()
.
Скрипт Python працює в головному потоці, і ми створюємо ще один потік (thread1
) для запуску функції some_func
, тому кількість активних потоків дорівнює двом, як видно з результату:
# Вивід
Виконується some_func...
2
Завершено виконання some_func.
Придивившись уважніше до результату, ми бачимо, що після запуску thread1
виконується перша інструкція виведення. Але під час операції затримки процесор перемикається на головний потік і виводить кількість активних потоків, не очікуючи завершення виконання thread1
.
Очікування завершення виконання потоків
Якщо ви хочете, щоб thread1
завершив виконання, ви можете викликати для нього метод join()
після запуску потоку. Це призведе до очікування завершення виконання thread1
без перемикання на головний потік.
import threading
import time
def some_func():
print("Виконується some_func...")
time.sleep(2)
print("Завершено виконання some_func.")
thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())
Тепер thread1
завершив виконання, перш ніж ми виводимо кількість активних потоків. Отже, запущено тільки основний потік, що означає, що кількість активних потоків дорівнює одному. ✅
# Вивід
Виконується some_func...
Завершено виконання some_func.
1
Як запускати кілька потоків у Python
Далі, ми створимо два потоки для виконання двох різних функцій.
Тут count_down
– це функція, що приймає число як аргумент і виконує зворотний відлік від цього числа до нуля.
def count_down(n):
for i in range(n,-1,-1):
print(i)
Визначимо count_up
, іншу функцію Python, яка рахує від нуля до заданого числа.
def count_up(n):
for i in range(n+1):
print(i)
📑 При використанні функції range()
з синтаксисом range(start, stop, step)
кінцева точка зупинки виключається за замовчуванням.
– Щоб виконати зворотний відлік від певного числа до нуля, можна застосувати від’ємне значення кроку -1 та встановити кінцеве значення на -1, щоб включити нуль.
– Аналогічно, для підрахунку до n необхідно встановити кінцеве значення на n + 1. Оскільки стандартні значення для початку та кроку дорівнюють 0 і 1 відповідно, можна використовувати range(n + 1)
для отримання послідовності від 0 до n.
Далі, ми ініціалізуємо два потоки, thread1
і thread2
, для виконання функцій count_down
та count_up
відповідно. Додаємо вивід в консоль та операції очікування для обох функцій.
При створенні об’єктів потоку, зверніть увагу, що аргументи цільової функції мають бути вказані як кортеж – параметр args
. Оскільки обидві функції (count_down
та count_up
) приймають по одному аргументу, потрібно явно вставити кому після значення. Це гарантує, що аргумент все ще передається як кортеж, бо наступні елементи будуть виводитися як None
.
import threading
import time
def count_down(n):
for i in range(n,-1,-1):
print("Виконується thread1....")
print(i)
time.sleep(1)
def count_up(n):
for i in range(n+1):
print("Виконується thread2...")
print(i)
time.sleep(1)
thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()
У виводі:
- Функція
count_up
виконується вthread2
і рахує до 5, починаючи з 0. - Функція
count_down
виконується вthread1
, виконуючи зворотний відлік від 10 до 0.
# Вивід
Виконується thread1....
10
Виконується thread2...
0
Виконується thread1....
9
Виконується thread2...
1
Виконується thread1....
8
Виконується thread2...
2
Виконується thread1....
7
Виконується thread2...
3
Виконується thread1....
6
Виконується thread2...
4
Виконується thread1....
5
Виконується thread2...
5
Виконується thread1....
4
Виконується thread1....
3
Виконується thread1....
2
Виконується thread1....
1
Виконується thread1....
0
Ви бачите, що thread1
і thread2
виконуються по черзі, оскільки обидва включають операцію очікування (сон). Коли функція count_up
завершує підрахунок до 5, thread2
більше не є активним. Отже, отримуємо результат, що відповідає лише thread1
.
Підсумок
У цьому посібнику ви дізналися, як використовувати вбудований модуль потокової обробки Python для реалізації багатопоточності. Ось короткий виклад основних висновків:
- Конструктор
Thread
можна використовувати для створення об’єкта потоку.threading.Thread(target=<callable>,args=(<tuple of args>))
створює потік, який запускає цільову функцію з аргументами, вказаними вargs
. - Програма Python працює в основному потоці, тому об’єкти потоку, які ви створюєте, є додатковими потоками. Ви можете викликати функцію
active_count()
, яка повертає кількість активних потоків в будь-який момент часу. - Ви можете запустити потік за допомогою методу
start()
для об’єкта потоку та дочекатися завершення виконання за допомогою методуjoin()
.
Ви можете попрактикуватися з іншими прикладами, налаштувавши час очікування, спробувавши іншу операцію введення-виведення тощо. Обов’язково реалізуйте багатопотоковість у ваших наступних проектах Python. Вдалого кодування!🎉