await for Tuple of task #1454
Replies: 38 comments
-
Could you not just a customer GetAwaiter for this? I havne't tried to make this compile, but something like: public static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tasks)
{
return Task.WhenAll(tasks.Item1, tasks.Item2).ContinueWith(arr => (tasks.Item1.Result, tasks.Item2.Result)).GetAwaiter();
} Then this would just work. Right? Or is there something i'm missing? -- Note: real example would properly handle edge cases for tasks completing. But the general idea is there. |
Beta Was this translation helpful? Give feedback.
-
Importantly, doing this in the language would necessarily couple the language to the BCL method |
Beta Was this translation helpful? Give feedback.
-
While I agree that the very common case is very easily solved via a library, it does make me think that maybe these are situations that the language would be better suited to handling. The reason being that such libraries would be necessarily limited to the number and type of permutations that it could handle. It's easy if you limit it to just |
Beta Was this translation helpful? Give feedback.
-
I think shapes should solve that @HaloFour. Shapes most accurately reflect what the language is actually doing. i.e. looking for something that matches an actual pattern rather than being an implementation of an interface. |
Beta Was this translation helpful? Give feedback.
-
For example, you could have an SEnumerable shape. Which would define the right shape and relationship between its members and the SEnumerator type it returned. Note: if/when shapes get done, we should validate this. It would be really good to ensure that all places where the language is implicitly using shapes internally can be expressible in an explicit manner. |
Beta Was this translation helpful? Give feedback.
-
That's a great point. If there was an |
Beta Was this translation helpful? Give feedback.
-
@HaloFour The implementation might be less efficient than The naive approach would be to await iteratively in a loop and stash failures, cancellations and results to be assembled at the end. More efficient would be to hook all awaitables with |
Beta Was this translation helpful? Give feedback.
-
@Thaina See my comment directly above. As @HaloFour stated well,
Task.WhenAll has access to the Task instances' internal ManualResetEventSlim and other synchronization primitives. It turns the entire operation into a very efficient single MRES wait. Whereas if you do this generally, through the SAwaitable interface, you introduce much more synchronization overhead. Not to mention the getting the exception handling correct. |
Beta Was this translation helpful? Give feedback.
-
@sharwell (from https://github.com/dotnet/corefx/issues/16010#issuecomment-383948077)
Assuming shapes are added to the language do you not feel it would be sufficient to have library support based around |
Beta Was this translation helpful? Give feedback.
-
Adding my 2¢- I don't think "let's wait for shapes" is a good plan for any given feature- given shapes is not scheduled for any specific language version and indeed may never happen. |
Beta Was this translation helpful? Give feedback.
-
I don't disagree. Shapes could become the next "source generators". |
Beta Was this translation helpful? Give feedback.
-
Arent Shapes at least being prototyped? Source Generators don’t even have a solid vision yet. |
Beta Was this translation helpful? Give feedback.
-
@MgSam aren't they a 8.0 candidate? |
Beta Was this translation helpful? Give feedback.
-
The conversation moved to awaiting a tuple of awaitables, but the title is outdated and looks like a duplicate of #380. I'm 100% in support of the idea of language support for awaiting a tuple of Task objects, now that the BCL team decided they didn't want the GetAwaiter extension methods on However, as I've had time to reflect on what it would mean to await a tuple of arbitrary awaitables rather than just Task objects, I've come to see it as a bad idea. For example, it would make it natural to write the following code. Where does the continuation execute? await (
FooAsync().ConfigureAwait(false), // Continue on the thread pool
BarAsync().ConfigureAwait(true), // Continue on the current synchronization context
Task.Yield(), // Continue on the current synchronization context but schedule it at the end of the queue
process.WhenExited(), // Continue on the thread watching process events
BazAsync().ContinueOn(someUISynchronizationContext)); // Continue on a non-current synchronization context
// Where am I? 😬 I think it would be a whole lot healthier to require Task objects and make the user explicitly do await (
FooAsync(),
BarAsync(),
process.WhenExited().AsTask()
BazAsync());
// On same synchronization context
await Task.Yield(); This still leaves the door open to support other awaitables down the road if it's deemed a performance issue to explicitly remind the reader of the semantics by using |
Beta Was this translation helpful? Give feedback.
-
IMO the benefit to this being a language feature disappears if all of the awaitables have to be a |
Beta Was this translation helpful? Give feedback.
-
If that's the only benefit of language integration, I'm voting against any language integration unless it has a good story for dealing with the code sample above. (I don't consider undefined behavior or 'last-to-complete wins' a good story because the code is confusing to read and my experience is that last-to-complete is the kind of subtlety that only shifts in production.) I think language integration would be nice for just the Task case because I don't like adding a NuGet package and using directives for something that makes sense out of the box. It would not surprise me if this doesn't matter very much to the LDT, but it is what it is. I don't think we're better off allowing arbitrary awaitables. |
Beta Was this translation helpful? Give feedback.
-
@jnm2 If your sample above was implemented by I think it should be the same, only that it would return a tuple instead of array |
Beta Was this translation helpful? Give feedback.
-
@Thaina |
Beta Was this translation helpful? Give feedback.
-
Task.WhenAll would be a useful implementation detail for the language capability to await a tuple, so long as all awaitables are Task objects. (It should be just that: an implementation detail.) If the compiler generates its own awaiting code, it will have to mimic Task.WhenAll in one important way: an exception thrown by one await should not cause other awaits to be skipped. Otherwise, exceptions from other awaits will be impossible to suppress before they trigger the global exception handler, even if the call site wraps everything in a catch-all. |
Beta Was this translation helpful? Give feedback.
-
I find I'm doing // start all async calls
var (taskX, taskF, taskS) = (db.Load<int>("X"), db.Load<float>("F"), db.Load<string>("S"));
// await the tasks to unwrap, yet still preserve the parallelism
var (x,f,s) = (await taskX, await taskF, await taskS); |
Beta Was this translation helpful? Give feedback.
-
@qrli This is a pitfall: var (x,f,s) = (await taskX, await taskF, await taskS); If |
Beta Was this translation helpful? Give feedback.
-
@jnm2 That's a good point. So, in practice, it should be similar to |
Beta Was this translation helpful? Give feedback.
-
@qrli Shouldn't we get |
Beta Was this translation helpful? Give feedback.
-
@qrli It no longer terminates the process by default, but it's still quite nasty when async Task Caller(CancellationToken cancellationToken)
{
try
{
await FooAsync(cancellationToken);
}
catch (Exception ex) // Just to make the point
{
// If more than one tasks in FooAsync ends up having exceptions, the first is in `ex`
// and the rest are now a ticking time bomb waiting for GC.
}
}
async Task FooAsync(CancellationToken cancellationToken)
{
var (taskX, taskF, taskS) = (db.Load<int>("X"), db.Load<float>("F"), db.Load<string>("S"));
// await the tasks to unwrap, yet still preserve the parallelism
var (x,f,s) = (await taskX, await taskF, await taskS);
} I've also seen processes go for minutes before the next GC. A very delayed error UI really confuses users (not to mention programmers) and obscures the actual steps to reproduce the crash. So: while in limited situations, namely top-level application code that has no caller other than a framework (Program.MainAsync, ASP.NET controller methods (maaaybe), desktop UI event handlers), or unless you know that the user won't be notified and long delays don't matter, it's not really a pattern that should be followed. And if any of that ever changes or the code moves around, there will be nothing to remind you to fix the time-bombing code. You might be interested in https://www.nuget.org/packages/TaskTupleAwaiter/ which enables safe, WhenAll-backed syntax: var (policy, preferences) = await (
GetPolicyAsync(policyId, cancellationToken),
GetPreferencesAsync(cancellationToken)
).ConfigureAwait(false); |
Beta Was this translation helpful? Give feedback.
-
@Thaina By design, tasks only throw the first exception when awaited. This avoids exception-handling pitfalls in C#. In my experience with async concurrency you aren't losing anything. I'd even say suppressing second and third exceptions is a helpful noise filter. Here's a thread that discusses this topic: buvinghausen/TaskTupleAwaiter#7 (comment) |
Beta Was this translation helpful? Give feedback.
-
@Thaina @jnm2 How does public static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tasks)
{
return Task.WhenAll(tasks.Item1, tasks.Item2).ContinueWith(arr => (tasks.Item1.Result, tasks.Item2.Result)).GetAwaiter();
} When |
Beta Was this translation helpful? Give feedback.
-
@qrli Observing is well-defined as either calling the Task.Exception getter or calling a method that causes the exception to be thrown, such as Since Cyrus' example is a less optimized version of the code in https://www.nuget.org/packages/TaskTupleAwaiter. TaskTupleAwaiter also gives you |
Beta Was this translation helpful? Give feedback.
-
Actually, @CyrusNajmabadi's version has a flaw. It erroneously wraps exceptions with AggregateException when awaited. In general, |
Beta Was this translation helpful? Give feedback.
-
For making a shorthand for parallel wait on multiple task. I think we should let
await
can be use on tuple of tasks toothis below code is sequencial, not parallel
For making parallel we need to put it in unnecessary variable
So I propose that we should be able to write it like this
If we write tuple syntax has some item being
Task<T>
in it, it then can put afterawait
and it would be transpiled, extract all task to it's real value, and replace the tuple with the result from taskThis would work like
WhenAll
but being tuple mean it would be able to deconstructed as each typesBeta Was this translation helpful? Give feedback.
All reactions