Thu 28 September 2017 | -- (permalink)
I mostly try to avoid programming with POSIX signals, in part because I was spoiled in my youth: even after decades of incremental improvements, POSIX signals have not really caught up to what ITS had back in the 1970s. But sometimes they're, if not the right tool, the least bad tool for a particular job. Case in point would be any program which supervises enough child processes that it needs to interleave process handling and I/O.
In C, this is tedious, but straightforward. In Python it gets more interesting, both because of Python's limited support for POSIX signals and because in Python it's easier to wrap a sane abstraction layer around the whole untidy mess.
YYMV, but to me, the obvious model for how POSIX signals behave is the
Event
abstraction, as used in threading.Event
,
tornado.locks.Event
, et cetera. This is a very simple abstraction
which one can use as part of the control structure for a simple event
loop, and its semantics are a good fit for POSIX signals.
So here's what a basic implementation might look like:
import signal, os
class Event(set):
def __init__(self):
super(Event, self).__init__()
self.throw = False
self.breakout = type("EventBreakout", (Exception,), {})
def handler(self, sig, ignored = None):
self.add(sig)
if self.throw:
self.throw = False
raise self.breakout
def wait(self, timeout = None):
if not self.is_set():
try:
self.throw = True
signal.alarm(0 if timeout is None else timeout)
while not self.is_set():
signal.pause()
except self.breakout:
pass
finally:
self.throw = False
signal.alarm(0)
# set.clear() already does what we want, no need to override it.
def set(self):
self.throw = False
self.handler(None)
def is_set(self):
return bool(self)
The critical features are:
-
The use of
raise
with a private exception class to effect non-local exits, and -
The use of a flag to restrict raising of the exception to situations where it does something helpful.
Paint and racing stripes have been left as an exercise for the reader.