Шаг 219.
Python: сборник рецептов.
Конкурентность. Как узнать, стартовал ли поток

    На этом шаге мы рассмотрим решение этой задачи.

Задача

    Вы создали поток и хотите узнать, когда он стартует.

Решение

    Ключевая возможность потоков - это то, что они выполняются независимо и недетерминистично. Это может вызвать сложности с синхронизацией, если другим потокам в программе перед выполнением следующих операций нужно знать, достиг ли некий поток некоторой точки в своем процессе выполнения. Чтобы решить эту проблему, используйте объект Event из библиотеки threading.

    Экземпляры Event похожи на флаг "sticky", который позволяет потокам ждать некоего события. Вначале событие установлено на 0. Если событие не установлено и поток ждет события, он будет заблокирован (т. е. уснет) до тех пор, пока событие не будет установлено. Поток, который устанавливает событие, разбудит все потоки, которые находятся в состоянии ожидания (если они есть). Если поток настроен ждать события, которое уже установлено, он просто продолжит выполнение.

    Вот пример кода, использующего Event для координации запуска потоков:

from threading import Thread, Event
import time

# Код для выполнения в независимом потоке
def countdown(n, started_evt):
    print("countdown starting")
    started_evt.set() 
    while n > 0:
        print("T-minus", n)
        n -= 1
        time.sleep(5)

# Создать объект события, который будет использован для сигнала о запуске
started_evt = Event()

# Запустить поток и передать событие запуска
print("Launching countdown")
t = Thread(target=countdown, args=(10,started_evt))
t.start()

# Ждать запуска потока
started_evt.wait()
print("countdown is running")

    Когда вы запустите этот код, сообщение "countdown is running" появится после сообщения "countdown starting". Это координируется путем события, которое заставляет главный поток ждать, пока функция countdown() не напечатает сообщение о запуске.

Обсуждение

    Объекты Event наилучшим образом подходят для одноразовых событий. Вы создаете событие, потоки ждут установки события, и когда оно установлено, Event выбрасывается. Хотя можно очистить событие, используя его метод clear(), безопасная очистка события и ожидание его повторной установки трудно координируется, а также может привести к пропущенным событиям, дедлокам или другим проблемам (в частности, вы не можете гарантировать, что запрос на очистку события после его установки будет выполнен до высвобождения циклов потоков для ожидания события).

    Если поток собирается снова и снова сигнализировать событием, вам лучше подойдет объект Condition. Например, этот код реализует периодический таймер, который могут отслеживать другие потоки - и видеть, что время истекло:

import threading
import time

class PeriodicTimer:
    def __init__(self, interval):
        self._interval = interval
        self._flag = 0
        self._cv = threading.Condition()

    def start(self):
        t = threading.Thread(target=self.run)
        t.daemon = True
        t.start()

    def run(self):
        '''
        Запустить таймер и уведомлять ждущие потоки после каждого интервала
        '''
        while True:
            time.sleep(self._interval)
            with self._cv:
                 self._flag ^= 1
                 self._cv.notify_all()

    def wait_for_tick(self):
        '''
        Ждать следующего срабатывания таймера
        '''
        with self._cv:
            last_flag = self._flag
            while last_flag == self._flag:
                self._cv.wait()

# Пример использования таймера
ptimer = PeriodicTimer(5)
ptimer.start()

# Два потока, синхронизирующихся по таймеру
def countdown(nticks):
    while nticks > 0:
        ptimer.wait_for_tick()
        print("T-minus", nticks)
        nticks -= 1

def countup(last):
    n = 0
    while n < last:
        ptimer.wait_for_tick()
        print("Counting", n)
        n += 1

threading.Thread(target=countdown, args=(10,)).start()
threading.Thread(target=countup, args=(5,)).start()

    Важнейшая возможность объектов Event в том, что они пробуждают все ждущие объекты. Если вы пишете программу, в которой вам просто нужна возможность пробуждать единственный ждущий поток, то, вероятно, лучше будет использовать объект Semaphore или Condition.

    Например, рассмотрим такой код, использующий семафоры:

# Поток-воркер
def worker(n, sema):
    # Ждет сигнала
    sema.acquire()
    # Выполняет работу
    print('Working', n)

# Создаем несколько потоков
sema = threading.Semaphore(0)
nworkers = 10
for n in range(nworkers):
    t = threading.Thread(target=worker, args=(n, sema,)) 
    t.start()

    Если вы запустите эту программу, стартует пул потоков, но ничего не произойдет, поскольку все они заблокированы, ожидая получения семафора. Каждый раз, когда высвобождается семафор, только один воркер проснется и запустится. Например:

>>> sema.release()
Working 0
>>> sema.release()
Working 1 
>>>

    Написание кода, который использует хитрую синхронизацию между потоками, может взорвать вашу голову. Более разумный подход к использованию потоков - работать с ними как с коммуницирующими задачами, используя очереди (или же как с акторами). Очереди описаны в следующем рецепте. Акторы будут описаны позже.

    На следующем шаге мы рассмотрим коммуникацию между потоками.




Предыдущий шаг Содержание Следующий шаг