На этом шаге мы рассмотрим создание и основные операции с потоками.
Вы хотите создавать и уничтожать потоки для конкурентного выполнения кода.
Библиотека threading может быть использована для выполнения любого вызываемого объекта Python в отдельном потоке. Чтобы сделать это, вы создаете экземпляр Thread и предоставляете ему вызываемый объект, который хотите выполнить. Вот простой пример:
# Код для выполнения в независимом потоке import time def countdown(n): while n > 0: print('T-minus', n) n -= 1 time.sleep(5) # Создать и запустить поток from threading import Thread t = Thread(target=countdown, args=(10,)) t.start()
Когда вы создаете экземпляр потока, он не начинает выполняться, пока вы не вызовете его метод start(), который вызывает целевую функцию с предоставленными вами аргументами.
Потоки выполняются в их собственных потоках системного уровня (т. е. потоке POSIX или потоке Windows), которые полностью управляются операционной системой. Будучи запущены, потоки выполняются независимо, пока целевая функция не вернет результат. Вы можете опросить экземпляр потока, чтобы проверить, запущен ли он:
if t.is_alive(): print('Still running') else: print('Completed')
Вы также можете запросить объединение с потоком. Это означает, что поток, в котором выполнен данный вызов, будет ждать завершения потока, к которому применен метод:
t.join()
Интерпретатор остается запущенным до тех пор, пока все потоки не будут завершены. Для долго выполняющихся потоков или бэкграундных задач, которые запущены все время, вам стоит попробовать создать поток-демон:
t = Thread(target=countdown, args=(10,), daemon=True)
t.start()
Демонические потоки не могут быть объединены. Однако они автоматически уничтожаются, когда завершается главный поток.
Помимо двух показанных операций, есть не так уж много других вещей, которые вы можете сделать с потоками. Например, нет операций для завершения потока, отправки сигнала потоку, нельзя настроить расписание работы или выполнить любую другую высокоуровневую операцию. Если вам нужны эти возможности, вам придется реализовать их самостоятельно.
Если вы хотите получить возможность завершать потоки, поток должен быть запрограммирован получать указания о выходе в определенных точках. Например, вы можете поместить свой поток в такой класс:
from threading import Thread import time class CountdownTask: def __init__(self): self._running = True def terminate(self): self._running = False def run(self, n): while self._running and n > 0: print("T-minus", n) n -= 1 time.sleep(5) c = CountdownTask() t = Thread(target=c.run, args=(10,)) t.start() time.sleep(20) print('About to terminate') c.terminate() # Сигнал завершения t.join() # Ждать реального завершения (если необходимо) print('Terminated')
Отправка запросов на завершение потоков может быть сложной в координации, если потоки выполняют блокирующие операции, такие как ввод-вывод. Например, если поток заблокирован на неопределенное время операцией ввода-вывода, он может никогда не вернуться, чтобы посмотреть, не убит ли он. Чтобы корректно разобраться с этим случаем, вам нужно аккуратно программировать потоки с использованием циклов с тайм-аутами. Например:
class IOTask: def terminate(self): self._running = False def run(self, sock): # sock это сокет sock.settimeout(5) # Установить тайм-аут while self._running: # Выполнить блокирующую операцию ввода-вывода с тайм-аутом try: data = sock.recv(8192) break except socket.timeout: continue # Продолжение обработки . . . # Завершено return
Из-за глобальной блокировки интерпретатора (global interpreter lock (GIL)) потоки Python ограничены моделью исполнения, которая разрешает только одному потоку выполняться в интерпретаторе в любой произвольно выбранный момент времени. По этой причине потоки Python не должны в общем случае использоваться для "тяжелых" вычислительных задач, где вам нужно добиться параллельного вычисления на нескольких процессорах. Они намного лучше подходят для обработки ввода-вывода или конкуретного выполнения в коде, производящего блокирующие операции (ожидание ввода-вывода, ожидание получения результатов из базы данных и т. п.).
Иногда вы можете встретить определение потоков через наследование от класса Thread. Например: f
rom threading import Thread class CountdownThread(Thread): def __init__(self, n): super().__init__() self.n = 0 def run(self): while self.n > 0: print('T-minus', self.n) self.n -= 1 time.sleep(5) c = CountdownThread(5) c.start()
Это работает, но вводит дополнительные зависимости между кодом и библиотекой threading. Поэтому вы можете использовать получившийся код только в контексте потоков, тогда как показанный ранее прием не имеет явной зависимости от threading. Путем освобождения вашего кода от таких зависимостей он становится доступным для использования в других контекстах - с потоками или без. Например, вы сможете выполнить ваш код в отдельном процессе, используя модуль multiprocessing:
import multiprocessing
c = CountdownTask(5)
p = multiprocessing.Process(target=c.run)
p.start()
. . .
Еще раз: это работает только в том случае, если класс CountdownTask был написан в нейтральной манере по отношению к средству обеспечения конкурентности (потоки, процессы и т. п.).
На следующем шаге мы рассмотрим определение старта потока.