-
-
Notifications
You must be signed in to change notification settings - Fork 343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should we deprecate trio.Event.clear? #637
Comments
Well … I'd start with adding that close-able Then, deprecate As to the websocket example: If you have a background task that sends data, shouldn't you use a |
@smurfix I'm not sure whether the |
(Note that it wouldn't have been useful in any of the cases above, except maybe @belm0's and my impression is that he needs something more complex anyway.) |
I have been using So I agree that a separate API for this use case might make things easier and clearer. |
@miracle2k oh cool, tell us more :-). When you say "the right way", do you mean doing |
I'm not sure having another special-purpose event is the right path in terms of ease of use and clear API. A counter example: it was trivial to make an event supporting level and edge trigger with any polarity and a simple property interface for the value (sorry, impl omitted): class BoolEvent:
"""Boolean offering the ability to wait for a value or transition."""
def __init__(self, value=False): ...
@property
def value(self): ...
@value.setter
def value(self, x): ...
async def wait_value(self, x=True):
"""Wait until given value."""
...
async def wait_transition(self, x=True):
"""Wait until transition to given value."""
... Using a property and not having the user bother about set/clear/get is very nice, as is making level and edge trigger very clear in the api and supporting all use cases an once. Implementation uses 4 ParkingLot instances, but these can be allocated lazily, and obviously halved if you remove the polarity option. And this same API can be used with arbitrary value types and predicates: foo = ValueEvent(12.5)
...
await foo.wait_transition(lambda x: 50 < x <= 100) |
@njsmith Yes its the 'wait for the next event' usecase you already mentioned. |
@miracle2k Do you have 1 waiter, or many? Is there data associated with the events? Do you have any trouble around shutting down? |
Further observation: there's a simple theoretical reason that [edited: was "if you have |
@njsmith Never had a any issue with shutting down. I think that is probably because my shutdowns, even if I want to controlled shutdown of a part of the task tree, always use trio's cancellation, so that should prevent wait() from deadlocking (I assume). I just ran across a case where I used this approach: It is essentially one task communicating updates to a second task, but I am not using a queue, because if the second task is too busy to respond to all updates, I just want it to work with the latest update once it has time. So the publisher does: current_state = 42
state_available.set() and the consumer does:
|
Hmm. I'd use a queue for that, adding a (synchronous) |
@miracle2k Ah interesting! So for this use case a pure wait-for-next-repeating-event isn't actually quite the same as what you're doing now. Specifically, consider the case where This way of using an (@smurfix though on another note, I've also recently been pondering the possibility of have a Another direction to generalize @miracle2k's case would be to make it multi-consumer, like a value that broadcasts updates. The interesting thing here is that you want to track, for each consumer, which was the latest update that it saw. Maybe something like, calling |
I was working on porting
So in short:
To be honest, I don't know if those details are really important, or are just a byproduct of the way So my first thought was, let's use a queue:
But:
I always ignore Conditions, so I tried to consider it. But we don't really need it - there is only one consumer. Even if there were more than one consumer, the asyncio version is written in such a way that the So instead I opted for an event:
It seems to work, but who knows? You guys probably do 😉 |
I was thinking about this again today, prompted by this talk. Starting around slide 67, @bcmills discusses the case of wanting to broadcast a "repeating transition", e.g. one task can toggle between idle and busy states, and another task wants to wait for the first task to enter its idle state. He has two solutions:
I also just read over @miracle2k's detailed post directly above this one, and it really feels to me like the solution is just... use a queue (or now, channel)? We have unbounded channels, so that takes care of the dynamic sizing issue, and for re-ordering the results, well, I'm not really seeing any compelling use cases for |
Conceptually "event" is just a flag. If we were to use name "flag",
would you object to "clear" function? Any thing can be abused by
opportunists.. In a bad way as well as in a good way.
|
Summary of event patterns & usage in our app:
We use our own event API's heavily: ValueEvent allows level and edge triggering on arbitrary predicates. The interesting thing here is that, say for |
On 04.06.19 12:55, John Belmonte wrote:
|clear()| immediately after |wait()|.
There is an ambiguity here. Will the first task that does this
wait/clear dance cause the other tasks to keep waiting until the next
`set`, or won't it? I can think of valid uses for both cases.
You can substitute the latter case with creating a new Event; the former
can probably be replaced with a semaphore.
…--
-- mit freundlichen Grüßen
--
-- Matthias Urlichs
|
Thank you. In our case |
I have a use-case where we are using an async def worker(run_permit, items):
for work in items:
await run_permit.wait()
await do_work(work)
async def pauser(run_permit, delay):
run_permit.clear()
await sleep(delay)
run_permit.set() It would be a bit awkward (but not too bad) to inject new events into the worker. I am not sure if this is really different than "tell worker(s) that work is ready" (vs "tell workers to chill for a bit"). |
@tacaswell I feel like a async def worker(run_permit, items):
for work in items:
async with run_permit:
await do_work(work)
async def pauser(run_permit, delay):
async with run_permit:
await sleep(delay) It has the advantage that it's guaranteed to actually pause the worker, which the original doesn't (consider if |
That is fair, but I was cheating a bit in the example async def worker(run_permit, items):
for work in items:
await run_permit.wait()
await do_work(work)
async def pauser(run_permit):
run_permit.clear()
async def resumer(run_permit)
run_permit.set() is closer to what we are actually doing and holding a lock not in a context block seems weirder to me than abusing Having a class which owns a |
@tacaswell I assume this is currently in asyncio, hence the reference to catching the cancelled exception? The other thing that |
@njsmith you have a talent for pulling the right threads to find the complexity in things ;) Yes, in asyncio. We handle the causality we care about in other ways (as the causality we care about involves some cross-thread coordination, because of course it involves threads....). I think a textual description of the use case is I have a long running iterative task that for "reasons" can't have backpressure applied to it (like a countdown timer for a rocket launch where the backpressure is a person yelling "hold" ;) ) directly via code. I want to ask on each pass through the loop "may I proceed, and if not tell me when I may" which seems like an On the other hand, a lock is "while I am running only I may touch this protected state", but there is no state to protect here. I think the I think I see how to do this with a Lock, a bool, and a Condition in a not super weird way. |
I've been following this thread with great interest and I find the discussion really exciting! (yes I know, I'm somehow rare in my tastes 😄 )
@tacaswell Your description is exactly the definition of a... semaphore! I mean the real-life semaphores. I'm not sure how should such a semaphore be implemented in Trio, but the semaphore explanation in the @bcmills talk refered by @njsmith I think that fits quite well to the idea. By contrary, for me an So if you see an HTH. Thinking in real-life examples makes me keep the models clean in my mind. And now here is my question: how would you implement a "real-life semaphore" for the @tacaswell's use case in Trio? |
… On Fri, Jun 7, 2019, 5:19 AM Xavier G. Domingo ***@***.***> wrote:
And now here is my question: how would you implement a "real-life
semaphore" for the @tacaswell <https://github.com/tacaswell>'s use case
in Trio?
|
@belm0 Sure, thanks! Glad to know that Trio already comes with batteries charged and uses good names for the good things! 👍 (Maybe I should start reading documentation as "normal" people do... 😜 ) @tacaswell Do you think that |
I wrote:
I reviewed my project's usage of First, there are only 13 instances of When we use trigger processing of data consumed by a dedicated task (non-queueing)The waiter must call
If a blocking operation is mistakenly placed between It's assumed that either we only need to queue one data item, or else data-overwrite semantics are acceptable. So a real queue seems overkill here as far as boilerplate, etc.-- but perhaps I can be convinced otherwise. trigger execution of non-reentrant work scheduled by a dedicated task (non-queueing)The waiter calls
It assumes that we want to ignore signals while the non-reentrant action is running. ... so yes, if |
A
Another way to think of it is "there's a token that only one task can hold at a time, and grants me permission to run".
Note that this case exactly match a classic reader-writer lock. ("Reader" really means "shared access" and "writer" means "exclusive access"; the names comes from a common use case but there's no obligation to actually read or write anything.) |
@belm0 thanks for looking into that!
This sounds like a natural place to use a (Actually, maybe we could even make it
This is exactly |
Thank you-- I think that's enough ammunition to attempt to preemptively disallow
I don't think our use cases need the "broadcast" or the "value", so the name is awkward, but the idea works. (Usually the value is a field in a local object, I'm not sure it's worth it to formally pass them around.) Maybe:
and
|
Ah, that makes sense (and I was even looking at reader-writer locks in a slightly different context this week). I am relieved that the thing I want has been wanted in the past and has a name 👍 . Thanks for tolerating my ramblings. |
I can't speak to Trio in particular, but in most concurrent systems a
If I understand Trio's model correctly, then that race cannot occur here because there is not a schedule point between |
Yes. For Python, single-threaded concurrency is a sweet spot due to the GIL. I have a large program with complex concurrency, yet no locks. We haven't lost any time to debugging concurrent memory access or race conditions.
It depends. Kotlin, for example, has structured concurrency with flexible controls on how coroutines are mapped to threads. It allows single-threading like Trio. Then combine the simplicity of single-thread concurrency with scale of many threads / processes / CPU's / machines via message passing. |
Especially after looking at our project's two use cases of repeated Events and abstracting them, the existence of Regarding the abstractions, I call them "repeated events". Not great, but reflects the historical usage and the fact that they keep Event's There is The other is |
Looking for a review in #1093 |
Further evidence that everyone gets Matthias Urlichs (@smurfix) says:
Turns out this was a classic |
Well, if that particular code had replaced the event with a new, cleared one, it would still have the exact same race condition … |
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
Event
that we calledclear
on, 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
Event
to 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 theclear
method 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
Event
objects to pass control back and forth between calls tosend_message
(in the main task) and a background writer task. Here's the writer task:https://github.com/HyperionGray/trio-websocket/blob/b787bf1a8a026ef1d9ca995d044bc97d42e7f727/trio_websocket/__init__.py#L300-L305
If another task calls
send_message
while the writer task is blocked insend_all
, then thesend_message
call willset()
the event again, and then whensend_all
completes, it gets unconditionally cleared, so we end up in the invalid state where there is data pending, butself._data_pending
is 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 stdlibthreading
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 aCondition
or aParkingLot
, or we could have aPeriodicEvent
type if it comes up enough... for a dedicatedPeriodicEvent
it might also make sense to have aclose
method of some kind to avoid race conditions at shutdown, where tasks callwait
after the last event has happened and deadlock.Event
object 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 anEvent
for each submission period. When a batch is gathered up for submission, theEvent
gets replaced, but the oldEvent
doesn'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.The websocket example above could be made correct by moving the
clear
so 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 aLock
would be better in any case: WebSocketConnection should implement the trio.abc.AsyncResource interface trio-websocket#3Are 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?The text was updated successfully, but these errors were encountered: