-
-
Notifications
You must be signed in to change notification settings - Fork 392
Description
It occurs to me that I've seen 4 different pieces of code try to use Event.clear lately, and they were all buggy:
-
My first suggestion for solving Add a blocked task watchdog #591 used an
Eventthat we calledclearon, and this created a race condition. (This was usingthreading.Event, but the principle is the same.) Details: Add task blocked watchdog. #596 (comment) -
New signal API: trio.open_signal_receiver #619 uses it correctly (I think), but it's only safe because we enforce that only one task can call
SignalReceiver.__anext__, which is subtle enough that I originally forgot to enforce it -
@belm0 tried to use
Eventto track a value that toggles between true and false, but then found it wasn't appropriate for what he needed after all. (Not exactlyEvent's fault, but if we didn't have theclearmethod then I'm sure he'd have realized more quickly that it wasn't what he was looking for.) -
@HyperionGray's websocket library tries to use
Eventobjects to pass control back and forth between calls tosend_message(in the main task) and a background writer task. Here's the writer task:If another task calls
send_messagewhile the writer task is blocked insend_all, then thesend_messagecall willset()the event again, and then whensend_allcompletes, it gets unconditionally cleared, so we end up in the invalid state where there is data pending, butself._data_pendingis not set.
Now, maybe this isn't Event.clear's fault, but it makes me wonder :-). (And this is partly coming out of my general reassessment of the APIs we inherited from the stdlib threading module, see also #322 and #573.)
The core use case for Event is tracking whether an event has happened, and broadcasting that to an arbitrary number of listeners. For this purpose, clear isn't meaningful: once an event has happened, it can't unhappen. And if you stick to this core use case, Event seems very robust and difficult to mis-use.
All of the trouble above came when someone tried to use it for something outside of this core use case. Some of these patterns do make sense:
-
If you have a periodic event, you might want to have the semantics of "wait for the next event". That can be done with an
Event, where waiters callawait ev.wait()and wakers callev.set(); ev.clear(). But it can also be done with aConditionor aParkingLot, or we could have aPeriodicEventtype if it comes up enough... for a dedicatedPeriodicEventit might also make sense to have aclosemethod of some kind to avoid race conditions at shutdown, where tasks callwaitafter the last event has happened and deadlock.- Another option in many cases is to model a periodic event by creating one
Eventobject per period. This is nice because it allows you to have overlapping periods. For example, consider a batching API, where tasks submit requests, and then every once in a while they get gathered up and submitted together. The submitting tasks want to wait until their request has been submitted. One way to do it would be to have anEventfor each submission period. When a batch is gathered up for submission, theEventgets replaced, but the oldEventdoesn't get set until after the submission finishes. Maybe this is a pattern we should be nudging people towards, because it's more general/powerful.
- Another option in many cases is to model a periodic event by creating one
-
The websocket example above could be made correct by moving the
clearso that it's right after thewait, and before the call that consumes the data (data = self._wsproto.bytes_to_send()). (It might be more complicated if the consuming call wasn't itself synchronous.) Soev.wait(); ev.clear()can make sense... IF we know there is exactly one task listening. Which is way outsideEvent's core use case. In this case, it's basically a way of "passing control" from one task to another, which is often a mistake anyway – Python already has a very easy way to express sequential control like this, just execute the two things in the same task :-). Here I think aLockwould be better in any case: WebSocketConnection should implement the trio.abc.AsyncResource interface trio-websocket#3
Are there any other use cases where Event.clear is really the right choice? Or where maybe it's not the right choice, but it's currently the best available choice?