-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathevents.py
199 lines (158 loc) · 6.13 KB
/
events.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
from functools import wraps
import threading
class UsageError(Exception):
"""
This is raised when you're trying to do something with an event that
isn't allowed.
"""
pass
def _atomic(func):
"""
A decorator to acquire an object's lock for the entirety of a function.
"""
@wraps(func)
def inner(self, *args, **kws):
with self._lock:
return func(self, *args, **kws)
return inner
class _ContextManagerMixin(object): # Inherit object for Python2 compatibility
"""
Inherit from this for Events that are created within a context manager.
"""
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, exception_traceback):
# Ignore any exceptions raised; they'll get reraised outside our
# context anyway. Just clean up our references.
self.destruct()
def destruct(self): # Call this when you want the Event to go out of scope.
pass
class Event(_ContextManagerMixin):
"""
You can pass these into `AnyEvent` or `AllEvent` below to join
these together. Beyond that, they act like `threading.Event` objects.
"""
def __init__(self):
self._event = threading.Event()
# We have a lock to make _set() and _clear() atomic. It is re-entrant
# so that we can create a callback that can atomically check various
# conditions before running set or clear, without deadlocking (used in
# AnyEvent and AllEvent, below).
self._lock = threading.RLock()
# We keep a mapping from other Event-like objects to pairs of (set,
# clear) nullary functions.
self._dependents = {}
def set(self):
self._set()
def clear(self):
self._clear()
@_atomic
def _set(self):
self._event.set()
# Note that the graph of all Event objects and their dependents is
# a DAG, and that setting or clearing any Event will only need to
# acquire the locks of the descendents of that Event. Consequently,
# we cannot have a deadlock: that would require two Events that are
# each others' descendents, and that cannot happen in a DAG.
for set_function, clear_function in self._dependents.values():
set_function()
@_atomic
def _clear(self):
self._event.clear()
# Similar to the implementation of set, we cannot have a deadlock
# here because the Events form a DAG.
for set_function, clear_function in self._dependents.values():
clear_function()
def is_set(self):
return self._event.is_set()
def wait(self, *args, **kws):
return self._event.wait(*args, **kws)
@_atomic
def _register(self, registrant, set_function, clear_function):
if registrant in self._dependents:
raise UsageError("Cannot register an event twice")
self._dependents[registrant] = (set_function, clear_function)
@_atomic
def _unregister(self, registrant):
if registrant not in self._dependents:
raise UsageError("Cannot unregister an event we never saw")
self._dependents.pop(registrant)
class _ComboEvent(Event):
def __init__(self, *events):
"""
We combine all the input events together when creating this one, using
the callbacks defined in subclasses. Think of this as an abstract base
class.
"""
super(_ComboEvent, self).__init__()
self._ancestors = events
with self._lock:
for event in self._ancestors:
event._register(self, self._set_callback, self._clear_callback)
self._initialize()
def destruct(self):
# Before we can go out of scope, we need to remove ourselves from all
# our ancestors, so that they don't hold references to us.
for event in self._ancestors:
event._unregister(self)
def set(self):
raise UsageError("Don't set combination events directly.")
def clear(self):
raise UsageError("Don't clear combination events directly.")
def _initialize(self): # Called to initialize the state at the beginning
raise NotImplementedError
def _set_callback(self): # Called when one of our ancestors is set
raise NotImplementedError
def _clear_callback(self): # Called when one of our ancestors is cleared
raise NotImplementedError
class InverseEvent(_ComboEvent):
"""
This event is the inverse of the event passed in on initialization. If the
base event is cleared, this is set and vice versa.
"""
def __init__(self, event):
# The difference between this __init__ and _ComboEvent.__init__ is that
# this one only accepts a single parent Event, whereas _ComboEvent can
# have arbitrarily many.
super(InverseEvent, self).__init__(event)
def _initialize(self):
if all(not event.is_set() for event in self._ancestors):
# There is just the one ancestor, but we look for "all" of them to
# simplify the statement.
self._set()
@_atomic
def _set_callback(self):
self._clear()
@_atomic
def _clear_callback(self):
self._set()
class AnyEvent(_ComboEvent):
"""
This Event gets set whenever any event in the constructor list is set, and
gets cleared when they're all cleared.
"""
@_atomic
def _set_callback(self):
self._set()
@_atomic
def _clear_callback(self):
if not any(event.is_set() for event in self._ancestors):
self._clear()
def _initialize(self):
if any(event.is_set() for event in self._ancestors):
self._event.set()
class AllEvent(_ComboEvent):
"""
This Event gets set whenever all the events in the constructor list are
set, and gets cleared when any of them are cleared.
"""
@_atomic
def _set_callback(self):
if all(event.is_set() for event in self._ancestors):
self._set()
@_atomic
def _clear_callback(self):
self._clear()
def _initialize(self):
if all(event.is_set() for event in self._ancestors):
self._event.set()