Защита критического раздела в многопоточной программе

У меня есть многопоточная программа Python (финансовая торговля), в которой определенные потоки выполняют критические разделы (например, в середине выполнения сделки). Поток, выполняющий критические разделы, является потоком демона. Основной поток программы захватывает SIGINT и пытается корректно выйти из программы, освобождая все ресурсы, удерживаемые дочерними потоками. Чтобы не допустить, чтобы основной поток внезапно завершал дочерние потоки; основной поток будет перебирать список дочерних объектов потока и вызывать их shutdown() функцию. Эта функция будет блокироваться до завершения критического участка потока перед возвратом.

Ниже приводится базовая реализация.

class ChildDaemonThread(Thread):

    def __init__(self):
        self._critical_section = False        
        # other initialisations

    def shutdown(self):
        # called by parent thread before calling sys.exit(0)

        while True:
            if not self._critical_section:
                break

            # add code to prevent entering critical section
            # do resource deallocation

     def do_critical_stuff(self):
         self._critical_section = True
         # do critical stuff
         self._critical_section = False

     def run(self):
         while True:
             self._do_critical_stuff()

Я не уверен, будет ли моя реализация работать, потому что, пока ChildDaemonThread выполняет критическую секцию через do_critical_stuff(), если родительский поток вызывает дочерний shutdown(), который блокируется до выполнения критической секции, то в этот момент используются два метода ChildDaemonThread run() и do_critical_stuff() звонили в то же время (я не уверен, законно ли это вообще). Это возможно? Моя реализация верна? Есть ли лучший способ добиться этого?


person Vino    schedule 21.03.2018    source источник


Ответы (1)


В этой реализации есть некоторые состояния гонки.

У вас нет гарантии, что основной поток проверит значение _critical_section в нужный момент, чтобы увидеть значение False. Рабочий поток может покинуть критическую секцию и снова войти в нее до того, как основной поток снова сможет проверить значение. Это может не вызывать проблем с корректностью, но может привести к тому, что ваша программа будет дольше завершать работу (поскольку, когда основной поток «пропускает» безопасное время для завершения, ему придется дождаться завершения другого критического раздела).

Кроме того, рабочий поток может повторно войти в критический после того, как основной поток заметил, что _critical_section равен False, но до того, как основной поток успеет вызвать завершение процесса. Это может вызвать серьезные проблемы с правильностью, поскольку это фактически мешает вашей попытке убедиться, что критический раздел завершен.

Конечно, программа также может аварийно завершить работу из-за какой-либо другой проблемы. Поэтому может быть лучше, если вы реализуете возможность восстановления после прерванной критической секции.

Однако, если вы хотите максимально улучшить эту стратегию, я бы предложил нечто подобное:

class ChildDaemonThread(Thread):

    def __init__(self):
        self._keep_running = True
        # other initialisations

    def shutdown(self):
        # called by parent thread before calling sys.exit(0)
        self._keep_running = False

     def do_critical_stuff(self):
         # do critical stuff

     def run(self):
         while self._keep_running:
             self._do_critical_stuff()
         # do resource deallocation


workers = [ChildDaemonThread(), ...]

# Install your SIGINT handler which calls shutdown on all of workers
# ...

# Start all the workers
for w in workers:
    w.start()

# Wait for the run method of all the workers to return
for w in workers:
    w.join()

Ключевым моментом здесь является то, что join будет блокироваться до завершения потока. Это гарантирует, что вы не прервете ни одной средней критической секции.

person Jean-Paul Calderone    schedule 21.03.2018
comment
Привет, большое спасибо. Ваш ответ имеет большой смысл. Я подумал, вместо того, чтобы использовать Boolean флаг _keep_running, я мог бы вам использовать экземпляр Event(), который является потокобезопасным. Что вы скажете по этому поводу? - person Vino; 22.03.2018
comment
Я не думаю, что вам нужен Event в этом случае. Установка атрибута в Python является поточно-ориентированной (один поток может выполнять self._keep_running = False, а другой поток читает self._keep_running; в этом случае вы никогда не получите противоречивый результат). Это правда, что что-то вроде Event может быть полезно (и часто необходимо) в определенных сценариях многопоточности. Но ... не этот, я думаю. Я могу быть не прав. Рассуждения о многопоточности - непростая задача. - person Jean-Paul Calderone; 22.03.2018