-
-
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
trio.wait_any() #1089
Comments
Isn't that reasonably simple to write? This ten-liner should do it:
The devil is in the details, though, because that's not code – that's a pattern which you most likely need to flesh out with your own code if you do need it. Also on that list: Futures; you can't get by without them when you write a client for an async network protocol, but the details WRT handling cancellation and aborts vary wildly, thus no generic common solution is so obvious as to merit inclusion into the core. We might want to create a separate section in the docs, or even a separate repository, for that kind of thing. |
If something is that useful, then we want to eliminate any friction to using it. No copy & paste or separate package. |
There's an example of essentially this in the docs already, and #472 is the issue for being more systematic about our doc examples. So I guess we can reserve this issue specifically for discussion on whether this should be built in to Trio. I guess a Trio-iffic name would be It seems plausible to me as a potential addition, but right now there's a backlog of foundational decisions to sort out that ( @belm0 I can understand theoretically why this abstraction makes sense, but that's not a substitute for the experience of actually using it in real-world situations. Are you able to share any examples of how you've used this "in the wild"? |
Note: this is linked in the first message, but in case anyone missed it, this discourse post has some more stats on how @belm0's group uses |
We haven't had a use case for return value from the winner. I guess the app is more about using concurrency for logic and control than data? As a name, Is my app an outlier as far as utility of this function? I tried looking for any large use of Trio in github as another sample, couldn't find anything. Anyway, some use snippets from our project: category: run task(s) in parallel along side a monitor which may abort them (very common) async def wander_loop():
"""Advance when wander is enabled."""
while True:
await wander_enabled_event.wait_value(True)
await wait_any(
partial(advance_with_recovery, velocity_max),
partial(wander_enabled_event.wait_value, False)
) # yelp until obstruction is cleared or wheels no longer deployed
await wait_any(
partial(voice_mutate, ...),
partial(locomotion.blocked_event.wait_value, False),
partial(physical_state_event.wait_value, lambda val: val is not WHEELS_DEPLOYED)
) category: just do stuff in parallel. There's a main task expected to exit first, the others are subsidiary. (As opposed to having a specific monitor to exit early.) await wait_any(
stretch_legs_animation,
partial(play_voice, ...),
) category: composing signals await wait_any(
object_near_left_wheel_event.wait_transition,
object_near_right_wheel_event.wait_transition
) That covers the roughly 200 cases. |
We have a pretty big Trio application that's open source: https://github.com/hyperiongray/starbelly I can't think of any places where I would use a There is definitely value in having a library of high quality implementations for these kinds of primitives. I'm ambivalent about whether they should be part of Trio core or not. |
run_race and gather (run_all) with return values are quite useful because these patterns repeat often. However, because these are missing from Trio, I find myself usually implementing something slightly more tailored, like "happy eyeballs" style approach instead of instantly running all the tasks.
This is not as simple as a one-liner call but still beats having to setup a nursery and write a task runner function that sends task return values back via a memory channel. And nothing stops adding ".any" and ".all" awaitables on the run_parallel return value, implementing the respective functions, if one does not wish to iterate over results. Another open question, relevant also to run_race and gather, and apparently already discussed elsewhere, is how tasks would be presented to such function. In this example I used coro objects, which isn't trionic -- but the alternatives, either forbidding arguments[1], or requiring several function-argument tuples -- aren't too hot either. [1] ... which leads to a hack I already use: |
@Tronic unfortunately, you can't hide a nursery inside an iterator, because an iterator can be abandoned at any time without warning, and because it becomes too ambiguous which code is inside the nursery's cancel scope. (See #264 and the many threads linked from there for more details, including multiple proposals for language changes... It's a whole thing.) You could have an
Heh, that's a clever trick! But I do think |
Exactly my thoughts... First I thought that it could be suddenly abandoned (and thought of abusing Here is the working version, with nursery moved outside: import trio
async def run_parallel(nursery, *coros):
class Result:
def __init__(self, idx, value = None, exc = None):
self.idx = idx
self.value = value
self.exc = exc
def __repr__(self):
val = f"raise {self.exc!r}" if self.exc else f"return {self.value!r}"
return f"<run_parallel.Result #{self.idx} {val}>"
async def result(self):
if self.exc: raise self.exc
return self.value
def __await__(self):
return self.result().__await__()
async def runner(sender, idx, coro):
async with sender:
try:
await sender.send(Result(idx, await coro))
except Exception as e: # Regular exceptions only?
await sender.send(Result(idx, exc=e))
sender, receiver = trio.open_memory_channel(0)
async with sender:
for idx, coro in enumerate(coros):
nursery.start_soon(runner, sender.clone(), idx, coro)
async for result in receiver:
try:
yield result
except GeneratorExit:
nursery.cancel_scope.cancel()
raise
async def mytask(delay, ret):
await trio.sleep(delay)
if isinstance(ret, Exception): raise ret
return ret
async def main():
tasks = mytask(3, Exception("task #0 not cancelled")), mytask(2, RuntimeError("task #1")), mytask(1, "I'm task #2")
async with trio.open_nursery() as nursery:
async for result in run_parallel(nursery, *tasks):
print("Result object:", repr(result))
# Any error handling here is optional
try:
print("Result value:", await result)
except RuntimeError as e:
print("Result exception:", repr(e))
break
trio.run(main) It is not entirely bad with an external nursery block, although 3+ lines and two indents is much more user code than a single-liner like |
And yes, it would seem if the async code running the |
FWIW, a async def gather(*tasks, name="gather"):
async def run_cr(idx, coro): results[idx] = await coro
async def run_fn(idx, fn, *args): results[idx] = await fn(*args)
results = len(tasks) * [None]
async with trio.open_nursery() as nursery:
for idx, task in enumerate(tasks):
if inspect.isawaitable(task):
n, runner = task.__qualname__, run_cr(idx, task)
else:
n, runner = task[0].__qualname__, run_fn(idx, *task)
nursery.start_soon(lambda: runner, name=f"{name}[{idx}] {n}")
return results |
While adding some documentation, I described wait_any's particular niche as follows:
This seems to cover a wide number of use cases and gives these trivial implementations high value. A key observation is that the functions passed in are often heterogeneous. We still don't have any cases in our project of running parallel homogenous things and needing return values. And for heterogeneous parallel calls with return values, the hand-coded nursery is king. |
Heterogeneous return values make sense in the context of wait_all (gather) but not wait_any. A hypothetical example: article_likes, current_weather = await gather(
(db.get_likes, ...),
(asks.get, 'http://weather.com/')
) In practice things like this usually end up being done sequentially because parallel async is too tedious (maybe not so with Trio nurseries but certainly with asyncio and even more so with non-async frameworks). |
I'll forgo asking for |
@belm0 thank you for packaging wait_any() in trio-util! I'm going to close this issue since it sounds like there's not anything left for us to decide here. |
Just to add another alternative: There is a async with trio.open_nursery() as nursery:
winner = await aioresult.wait_any([aioresult.ResultCapture.start_soon(nursery, my_func, i) for i in range(10)])
nursery.cancel_scope.cancel()
print("Winning result:", winner.result()) |
However since Currently I'm considering extending the results = await wait_any(foo, # We don't care about foo()'s return value.
bar=bar, # If bar() ends the wait, capture its return value.
baz=baz) # etc.
if results.bar: ... Where |
I'm going out on a limb: wait_any() should be part of the Trio package.
The bar for utility-like things should be high for Trio. Points about
wait_any()
:It's actually detrimental to Trio to not have this available to all programs out of the box.
The text was updated successfully, but these errors were encountered: