Я пишу код, который хочу использовать в интерактивной оболочке, такой как IPython, это означает, что код должен иметь возможность обрабатывать полностью асинхронное и неожиданное исключение, такое как KeyboardInterrupt
, и по-прежнему функционировать после него. Думаю, мне следует использовать контекстные менеджеры для обработки блокировки и разблокировки.
Проблема в том, что если во время выполнения метода __enter__
или __exit__
возникает исключение, и мне нужно получить несколько блокировок в методе __enter__
и снять несколько блокировок в __exit__
?
Насколько я понимаю, вызовы кода C являются атомарными, а все инструкции байт-кода являются атомарными. Таким образом, вызов thread.Lock.acquire()
не будет прерван, как и присвоение результата этого вызова переменной. Но у нас нет гарантии, что исключение не будет вызвано между возвратом из acquire()
и присвоением возвращаемого значения переменной (поскольку это обрабатывается двумя разными операциями на уровне байт-кода).
Другими словами, такой код:
import threading
lock = threading.Lock()
while True:
locked = False
try:
locked = lock.acquire()
except KeyboardInterrupt:
if locked:
lock.release()
locked = False
raise
finally:
if locked:
lock.release()
не всегда отпускает lock
на Ctrl+C.
аналогично, следующий скрипт иногда не проходит последнюю проверку assert
:
import sys
import threading
class ComplexLock():
def __init__(self):
self.lock_a = threading.Lock()
self.lock_b = threading.Lock()
def __enter__(self):
self.lock_a.acquire()
self.lock_b.acquire()
def __exit__(self, exc_type, exc_value, traceback):
self.lock_b.release()
self.lock_a.release()
def state_consistent(self):
return not self.lock_a.locked() and not self.lock_b.locked()
a = ComplexLock()
assert a.state_consistent()
try:
while True:
with a:
pass
except KeyboardInterrupt:
sys.exit(0)
finally:
assert a.state_consistent()
(примечание: я не могу использовать lock.locked()
вместо locked
, так как я не знаю, удерживает ли блокировку текущий поток; lock
является глобальным и общим, locked
является локальным для потока)
Итак, вопрос в том, как обращаться с KeyboardInterrupt
таким образом, чтобы не повредить глобальные объекты?
Практический пример использования:
Учтите, что вы хотите вычислить e**x
, но ради этого аргумента время вычисления велико и зависит от значения x
.
Как поставщик библиотеки, я предоставляю e
в качестве экспорта, чтобы пользователь мог
from example_library import e
e**12
Теперь, поскольку вычисления занимают много времени, я хочу внутренне кэшировать некоторые предварительно вычисленные переменные (не результаты, я знаю о lru_cache, и это не сработает для моего реального случая использования). Так что, хотя e
не меняется внешне, оно меняется внутренне. Но это детали реализации, неизвестные пользователю.
Поэтому, когда пользователь делает ^C, потому что e**10000
занимает слишком много времени, пользователь может ожидать, что выполнение e**12
после этого ^C будет работать нормально.
Но это не сработает, если блокировка, используемая для синхронизации кеша предварительно вычисляемых переменных, была заблокирована, но не освобождена во время предыдущего вычисления.
@MisterMiyagi, как я уже сказал, это для случая использования IPython, когда у вас есть глобальные объекты с блокировками, вы хотите, чтобы пользователь мог прервать долгий расчет с помощью Ctrl + C и при этом иметь согласованную среду. Итак, нет, KeyboardInterrupt
не означает завершение процесса.
Теперь, когда мы выяснили, что прекращение — это не то, что вы ожидаете, можете ли вы уточнить, что вы ожидаете на самом деле? Каково ожидаемое поведение? Пока я вижу только то, чего вы не ожидаете. Что вы ожидаете, если блокировка удерживается дочерним потоком, который естественным образом защищен от KeyboardInterrupt? Это должно работать только для ipython или обычных программ?
@MisterMiyagi ситуация такова, что пользователь может начать длительный расчет с небольшим критическим разделом, в котором используются глобальные объекты. После Ctrl+C пользователем я не хочу, чтобы глобальные объекты не работали, потому что их состояние блокировки непоследовательно (заблокировано, несмотря на то, что код не запущен). Он должен работать как для интерактивного режима интерпретатора Python, так и для IPython.
Является ли вычисление на самом деле дорогостоящим, т.е. вы хотите, чтобы ^C просто возвращал приглашение или он также должен убить вычисление?
на самом деле, я думаю, что это должно работать и в обычном питоне: я могу представить ситуацию, когда приложение позволяет пользователю выполнять Ctrl + C, чтобы пропустить некоторые вычисления, отбросить эти результаты и продолжить другие, несвязанные вычисления.
@MisterMiyagi расчет стоит дорого, я хочу прервать его в произвольном месте
Означает ли это, что вы также должны иметь возможность прервать его, если он не работает в основном потоке? Использование блокировки для защиты общего состояния между потоками, по-видимому, подразумевает это.
@MisterMiyagi блокировка есть, потому что я не знаю, будет ли код использоваться в многопоточном приложении или нет. Обычно ^C происходит только в однопоточном коде, но я не могу этого предположить. Если пользователь запускает поток, который выполняет вызов моей библиотеки, который занимает много времени, он не сможет прервать его немедленно, но это верно для всего кода Python, так что это неудивительно. Неспособность выполнять вычисления с объектами, которые кажутся неизменяемыми (но поскольку детали реализации внутри не являются таковыми) после ^C, является неожиданностью и, возможно, ошибкой.
Хорошо, это, вероятно, то, с чем можно поработать (все же было бы неплохо иметь требования в реальных вопросах). Однако имейте в виду, что, поскольку многопоточность кажется не частью вашего предполагаемого варианта использования, обычным способом было бы документировать ваши вычисления как не потокобезопасные. Это означает, что пользователи соглашаются на защиту потоков, подходящую для их варианта использования, вместо того, чтобы вам приходилось гадать, что это за варианты использования и как их защитить.
Давайте продолжим обсуждение в чате.
Я не думаю, что есть хороший способ справиться с этим с помощью KeyboardInterrupt
, но вы можете справиться с этим с помощью сигналов.
В Python KeyboardInterrupt
вызывается обработчиком по умолчанию для SIGNINT
(см. здесь). Вы можете определить свой собственный обработчик сигналов, чтобы делать что-то еще, и обрабатывать его, когда хотите.
import os
import signal
import threading
interrupted = False
def signal_handler(signal_number, frame):
global interrupted
interrupted = True
original_signal_handler = signal.signal(signal.SIGINT, signal_handler)
lock = threading.Lock()
while not interrupted:
locked = False
try:
locked = lock.acquire()
# Do something while holding the lock
finally:
if locked:
lock.release()
signal.signal(signal.SIGINT, original_signal_handler)
if os.name == "nt":
# Windows needs a special case because kill() doesn't send SIGINT.
os.kill(os.getpid(), signal.CTRL_C_EVENT)
else:
os.kill(os.getpid(), signal.SIGINT)
Я не тестировал версию для Windows, но она должна работать.
Вы должны позаботиться о том, чтобы ваш код быстро завершал работу после установки interrupted
.
проблема в том, что я делаю это в библиотеке, а не в приложении, поэтому я не могу предположить, что могу использовать обработчик SIGINT
Вы предполагаете, что KeyboardInterrupt
будет повышено, а это означает, что вы предполагаете, что приложение, использующее вашу библиотеку, не будет устанавливать свой собственный обработчик сигналов. В таком случае, не могли бы вы установить свой собственный обработчик, сделать свое дело, а затем восстановить обработчик по умолчанию и снова отправить сигнал?
Как мне сделать так, чтобы код, который я оборачиваю в контекстный менеджер, все еще мог быть прерван с помощью Ctrl + C?
У вас все еще есть пользовательский обработчик между lock.acquire()
и lock.release()
, поэтому «# Сделайте что-нибудь, удерживая блокировку» не будет прервано
Вы имеете в виду, что вы перезваниваете коду пользователя, удерживая блокировку?
Давайте продолжим обсуждение в чате.
На самом деле, похоже, что это невозможно в общем смысле, это ошибка в питоне: https://bugs.python.org/issue29988 а также это известный недостаток, см. PEP 419 (к сожалению, отложено ). И конкретнее для этого случая: https://bugs.python.org/issue31388
Суть в том, что даже если __enter__
и __exit__
являются функциями C, не гарантируется, что __exit__
будет вызвана, даже если __enter__
будет выполнена успешно, мы можем просто исправить это на уровне Python, чтобы сделать это менее вероятным, а не невозможным
реализуя наш собственный обработчик сигналов и откладывая доставку сигнала, мы можем сделать его достаточно надежным, но только в том случае, если ожидается, что критический участок будет коротким, поэтому отсрочка не повлияет на пользователя.
чтобы продолжить пример из вопроса, похоже, что-то вроде этого работает, как и ожидалось, даже если мы введем потоки в микс:
import sys
import signal
import threading
class ComplexLock():
def __init__(self):
self.lock_a = threading.Lock()
self.lock_b = threading.Lock()
def __enter__(self):
if threading.current_thread().__class__.__name__ == '_MainThread':
# only MainThread can handle signals
self.signal_received = False
self.old_handler = signal.signal(signal.SIGINT, self._handler)
self.lock_a.acquire()
self.lock_b.acquire()
def __exit__(self, exc_type, exc_value, traceback):
self.lock_b.release()
self.lock_a.release()
if threading.current_thread().__class__.__name__ == '_MainThread':
signal.signal(signal.SIGINT, self.old_handler)
if self.signal_received:
self.old_handler(*self.signal_received)
def _handler(self, sig, frame):
self.signal_received = (sig, frame)
def state_consistent(self):
return not self.lock_a.locked() and not self.lock_b.locked()
lock = ComplexLock()
assert lock.state_consistent()
import time
def countdown(x):
while x[0]:
with x[1]:
pass
param = [True, lock]
t = threading.Thread(target=countdown, args=(param, ))
t.start()
try:
while True:
with lock:
pass
except KeyboardInterrupt:
param[0] = False
t.join()
sys.exit(0)
finally:
assert lock.state_consistent()
(очевидно, есть гонки, когда доставляется несколько сигналов, и некоторые из них могут быть сброшены, но это меньшая проблема, чем нарушенная среда после Ctrl + C)
Если вы намеренно не подавите исключение (через
signal
), у вас не будет возможности полностью его обработать; поскольку он асинхронный, он может быть вызван снова при обработке, и теоретически это может произойти вплоть до произвольной рекурсии. Можете ли вы уточнить, что вы считаете правильным обращением сKeyboardInterrupt
? Поскольку смыслKeyboardInterrupt
состоит в том, чтобы сигнализировать о завершении работы, кажется, что правильным ответом является завершение приложения; это, естественно, избавит от необходимости снимать внутрипроцессные блокировки.