runtime: Support await'ing a Task without throwing
EDITED 5/24/2023 by @stephentoub: Latest proposal at https://github.com/dotnet/runtime/issues/22144#issuecomment-1561983918
namespace System.Threading.Tasks;
public class Task
{
+ public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitOptions options);
}
+[Flags]
+public enum ConfigureAwaitOptions
+{
+ None = 0,
+ ContinueOnCapturedContext = 0x1,
+ SuppressExceptions = 0x2,
+ ForceYielding = 0x4,
+ ForceAsynchronousContinuation = 0x8,
+}
(I’m not sure yet if we actually need to ship ForceAsynchronousContinuation now. We might hold off if we don’t have direct need in our own uses.)
EDIT: See https://github.com/dotnet/runtime/issues/22144#issuecomment-943445439 for an up-to-date API proposal.
namespace System.Threading.Tasks;
+[Flags]
+public enum TaskAwaitBehavior
+{
+ Default = 0x0,
+ NoContinueOnCapturedContext = 0x1,
+ NoThrow = 0x2,
+}
public partial class Task
{
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
+ public ConfiguredTaskAwaitable ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}
public partial class Task<TResult>
{
public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
+ public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}
public partial struct ValueTask
{
public ConfiguredValueTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
+ public ConfiguredValueTaskAwaitable ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}
public partial struct ValueTask<TResult>
{
public ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
+ public ConfiguredTaskAwaitable<TResult> ConfigureAwait(TaskAwaitBehavior awaitBehavior);
}
Original post
Currently there isn't a great way to await a Task without throwing (if the task may have faulted or been canceled). You can simply eat all exceptions:try { await task; } catch { }
but that incurs the cost of the throw and also triggers first-chance exception handling. You can use a continuation:
await task.ContinueWith(delegate { }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
but that incurs the cost of creating and running an extra task. The best way in terms of run-time overhead is to use a custom awaiter that has a nop GetResult:
internal struct NoThrowAwaiter : ICriticalNotifyCompletion
{
private readonly Task _task;
public NoThrowAwaiter(Task task) { _task = task; }
public NoThrowAwaiter GetAwaiter() => this;
public bool IsCompleted => _task.IsCompleted;
public void GetResult() { }
public void OnCompleted(Action continuation) => _task.GetAwaiter().OnCompleted(continuation);
public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation);
}
...
await new NoThrowAwaiter(task);
but that’s obviously more code than is desirable. It’d be nice if functionality similar to that last example was built-in.
Proposal
Add a new overload of ConfigureAwait, to both Task and Task<T>. Whereas the current overload accepts a bool, the new overload would accept a new ConfigureAwaitBehavior enum:
namespace System.Threading.Tasks
{
[Flags]
public enum ConfigureAwaitBehavior
{
NoCapturedContext = 0x1, // equivalent to ConfigureAwait(false)
NoThrow = 0x2, // when set, no exceptions will be thrown for Faulted/Canceled
Asynchronous = 0x4, // force the continuation to be asynchronous
... // other options we might want in the future
}
}
Then with ConfigureAwait overloads:
namespace System.Threading.Tasks
{
public class Task
{
...
public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitBehavior behavior);
}
public class Task<TResult> : Task
{
...
public ConfiguredTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitBehavior behavior);
}
}
code that wants to await without throwing can write:
await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow);
or that wants to have the equivalent of ConfigureAwait(false) and also not throw:
await task.ConfigureAwait(ConfigureAwaitBehavior.NoCapturedContext | ConfigureAwaitBehavior.NoThrow);
etc.
From an implementation perspective, this will mean adding a small amount of logic to ConfiguredTaskAwaiter, so there’s a small chance it could have a negative imp
Alternatives
An alternative would be to add a dedicated API like NoThrow to Task, either as an instance or as an extension method, e.g.
await task.NoThrow();
That however doesn’t compose well with wanting to use ConfigureAwait(false), and we’d likely end up needing to add a full matrix of options and supporting awaitable/awaiter types to enable that.
Another option would be to add methods like NoThrow to ConfiguredTaskAwaitable, so you could write:
await task.ConfigureAwait(true).NoThrow();
etc.
And of course an alternative is to continue doing nothing and developers that need this can write their own awaiter like I did earlier.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 33
- Comments: 82 (79 by maintainers)
Change
ConfigureAwaitBehaviortoAwaitBehavior?task.ConfigureAwait(AwaitBehavior.NoCapturedContext | AwaitBehavior.NoThrow)Please consider a shorter enum name that doesn’t start with a verb, e.g. what @willdean proposed (
AwaitBehavior).ConfigureAwait(ConfigureAwaitBehavior.NoThrow)is a lot of redundancy.A little bit of bike-shedding, but should the enum be
AwaitBehaviorrather thanConfigureAwaitBehavior?After all, it’s describing the behavior of the await, not the behavior of the the
ConfigureAwaitfunction - the latter is just there to configure the behavior.Video
We discussed this today, and one of the main feelings is that the ConfigureAwait(NoThrow) feels like a pit of failure. One piece of that is it invalidates the nullability annotations, since nulls will start appearing out of non-null-returning members.
Something like an extension method that changes from an
awaitable<T>to anawaitable<(T?, Exception)>, or at leastawaitable<(bool, T?)>might be reasonable, but that can’t be described as a flags flag. This is, in particular, important for the ValueTask versions, since the Exception cannot otherwise be recovered.We probably want to try this out on non-public code first to get an idea of what’s really needed.
I keep coming across places where this would be valuable, and I’d like to get a solution into .NET 8. I think we’ve been making this too complicated…
We don’t need to support ValueTasks, which are problematic because you can’t look at them after you await them, and so any model for ValueTask would need to entail returning exception information, which is unfamiliar in the model; we can instead just say “if you need this and you have a ValueTask, call AsTask first”. Yes, it may allocate, but the use cases where you need this are more niche, you can check first whether it already completed, and AsTask for something that hasn’t yet completed will only allocate if it’s backed by an IValueTaskSource, which is itself currently problematic for such configuration because it doesn’t provide a way currently to get at the Exception without throwing it.
Further, we don’t need to support TResults. Previous concerns with TResult includes not knowing whether you can actually trust the returned TResult, and any nullability annotations possibly being wrong. But
Task<TResult>derives fromTask, so any support forTask“just works” forTask<TResult>, and in particular without violating anything about theTResult’s nature.So, if we just expose this support for
Task, we address the 99% case and we can make that work well.Thus my proposal is simply this:
Notes:
ConfiguredTaskAwaitablebut instead namedConfiguredWithOptionsTaskAwaitable(name can obviously be debated).Task<Task>)..ConfigureAwaitwhere they didn’t previously have one in order to suppress exceptions and neglects to also add ContinueOnCapturedContext. But to change that, we’d need to invert the name of ContinueOnCapturedContext.Examples:
I thought I’d revive this issue for 7.0.0. Given we’ve merged timeouts and cancellation via the
WaitAsyncapproach in #47525, based on the prototype in 416cc3c07697082e921fd496e3b9871e92202946 I believe the following might be a viable API shape:Open Questions
NoThrowbe supported inValueTask’s?NoThrowhave impact on nullability annotations of the returned type? (would require authoring dedicated awaitable/awaiter types)cc @stephentoub
I like the composable style of
await task.ConfigureAwait(true).NoThrow()Could we then have more clearly-named alternatives to ConfigureAwait(bool) which didn’t need a bool?
To someone who hasn’t been completely steeped in TPL, etc. from the outset I don’t think
ConfigureAwait(false)is intuitive at all - does “false” mean it isn’t configured? Does it mean it isn’t awaiting? (Rhetorical, I don’t need an explanation myself)I know a renamed
ConfigureAwaitalternative isn’t the subject of this issue, but could it go hand-in-hand with this change? What might a good name for ‘ConfigureAwait(false)’ be (assuming there’s any agreement that it could be improved)I’m planning to submit a pr to aspnetcore once it moves to a new enough runtime.
I’m surprised by that. It’s rare in my experience to care that an operation has completed but to also not care whether it was successful, what result it produced if any, or what error it encountered if any. Such scenarios certainly exist, but in my experience they’re very far from the majority. I don’t believe we should ship an API for this that has such constraints.
This is how
Task.Yield()is implemented:https://github.com/dotnet/runtime/blob/8a52f1e948b6f22f418817ec1068f07b8dae2aa5/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.cs#L50
We can decide, but I think the answer should be “yes”. If someone doesn’t want that, they just check task.Exception, just as they can today.
It would be
default(T). With NoThrow, you have to check the status oftaskafterwards to know whether it was successful, faulted, or canceled, and you’d only use the result if it was valid.A very similar issue for synchronous waiting: https://github.com/dotnet/corefx/issues/8142.
💭 I really wish there was a way to return a cancelled result from an
asyncmethod without throwing the exception… related to dotnet/roslyn#19652.Rather than
NoCapturedContext, I would prefer to see:Another helpful option would be
Yield, which alters the behavior in cases where the antecedent is already complete in order to force a yield.FWIW, I disagree with the assertions in that comment that the exception is irrelevant. First, an action the user’s code initiated failed with an arbitrary exception; from my perspective, it shouldn’t just be eaten by the implementation because the user happened to stop enumerating (and, yes, while exceptions are discouraged from Dispose, sometimes it’s the lesser evil). Second, the unobserved exception is then likely going to raise the TaskScheduler.UnobservedTaskException event, assuming the operation is actually backed by a task.
I realize this isn’t the main point of the discussion, nor do we need to debate the semantics of an API we’re not going to ship. I’m simply trying to highlight that it’s important we factor in ValueTask.
Thanks for the discussion.
I believe the options that return a
ConfiguredTaskAwaitable<TResult>(or similar) were already dismissed as too risky when the task fails. There is one very specific implementation in dotnet/runtime: https://github.com/dotnet/runtime/blob/62e87b43207a17caa7b3d3260c1f58f155f5284e/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs#L738 For general use I propose just returning the task as the result of the awaitable:This has the additional benefit of not needing to store the task in a captured variable (it’s instead only temporarily stored in the awaiter variable, which is anyway needed). A different overload+struct could add
continueOnCapturedContextparameter support also.I’m a little surprised about that. I’d have expected the Asynchronous flag to need to be checked in IsCompleted and the NoThrow flag to need to be checked in GetResult, resulting in several more branches on the hot path as well as either more code that gets inlined into lots of call sites or methods that once were inlined no longer being so.
Those don’t use ConfigureAwait.
The definition of IsCompleted is up to the awaiter. In this case, that definition includes not supporting synchronous completion. It’s also in the System.Runtime.CompilerServices namespace because it’s part of the async/await infrastructure and meant to be consumed by the compiler via a very specific pattern. IsCompleted is a simple, understandable name, but if it helps you sleep better 😃, think of it as “StateMachineShouldProceedWithoutHookingUpACallback”.
Sounds reasonable.
I mean, strictly speaking it is pretending to not be completed so that the state machine is forced to pass its continuation. I’d probably be less concerned if this weren’t a public property.
I was motivated by the assumption that returning
defaultfor non-nullable reference types is more harmful than value types. But then again value types can contain references so probably not that important. Alternatively, we could perhaps consider adding an analyzer/fixer that detects statements of the formFoo foo = await task.ConfigureAwait(AwaitBehavior.NoThrow);and recommends changing those toFoo? foo = await task.ConfigureAwait(AwaitBehavior.NoThrow);.Oh, wait, “without awaiting”? If there’s no await, the above is entirely a nop.
I prefer the ConfigureAwait approach, e.g.
await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow | ConfigureAwaitBehavior.NoCapturedContext);, rather than having lots of different methods for all the various ways we might want to support awaiting and that all need to compose in some fashion (this is why the method is called ConfigureAwait today, because the idea was it could be extended in this manner in the future). This also ties in with #27723, for example, where we’d have an overload that would let you doawait task.ConfigureAwait(timeout), an overload that would let you pass a cancellation token, an overload that would let you pass a timeout and a cancellation token and a ConfigureAwaitBehavior, etc. It all ends up just being about configuring the await. #32030@eiriktsarpalis, are you still driving the design of this?