-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Support await'ing a Task without throwing #22144
Comments
Rather than
Another helpful option would be |
💭 I really wish there was a way to return a cancelled result from an |
That's unrelated to this issue, though. You're talking about how you transition the returned Task to be in a canceled state. This issue is about the consuming side, regardless of how the Task-returning method was implemented... it may not have been using async/await at all. |
I like the composable style of 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 I know a renamed |
But that means that code like: await task.ConfigureAwait(ConfigureAwaitBehavior.NoThrow); would end up behaving like continueOnCapturedContext==false, and thus not incorporating the default for that option. |
...which is the exact reason Clean Code (and a lot of similar books/posts) recommends against them. And if I ever get around to finishing that writeup of the language idea I have, raw Boolean parameters would be explicitly disallowed, in favor of some encapsulating value (like a shortcut enum declaration or something) |
A very similar issue for synchronous waiting: https://github.com/dotnet/corefx/issues/8142. |
I think the question here is whether it's better to have clean and simple API or whether it's better if the code that uses the API is simple and short. With On the other hand, with something like |
Change
|
If that's really all the choice is, then it seems to me one should almost always favour putting the complexity in the framework rather than the consuming app - otherwise what's the framework for? If everything has to be duplicated on |
Where did we land on this API wise? Do we like the ConfigureAwait approach or the NoThrow extension method (I prefer that). |
I prefer the ConfigureAwait approach, e.g. @eiriktsarpalis, are you still driving the design of this? |
I started working on a prototype a few months back, but it got dropped on the floor. I'll see if I can revive it. |
A developer might want one of several behaviors:
To illustrate, if a developer writes In Orleans, we often want behavior 3, and have an If we had something similar to Rx's TaskResult<T> result = await task.AsNotification();
return result switch
{
(Canceled, _, _) => null, // Ignore if the task was canceled.
(Faulted, _, ex) => await task, // throw if the task faulted
(Completed, value, _) => value,
_ => throw new NotSupportedException("...")
};
// Given deconstruction, it could be:
var (status, value, exception) = await task.AsNotification(); That doesn't satisfy behavior 3, but it satisfies behavior 1, 2, and 4, I believe. |
It would be |
I also prefer this approach. Considering how clean it is, even if it starts out slower in some sense I'd be interested to rule out JIT improvements targeting common patterns before letting it impact the API. |
If a developer writes |
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. |
My instinct here is yes. Normally the |
Oh, wait, "without awaiting"? If there's no await, the above is entirely a nop. |
A little bit of bike-shedding, but should the enum be After all, it's describing the behavior of the await, not the behavior of the the |
.... Side note: If we ever get a |
My intuition would be "no" because the developer already consciously attended to this task and decided in his head how he wants to deal with it. When |
I don't believe we know that just from being told the await shouldn't throw. In my experience, such a request often comes from wanting to join and then handle failures in some special way and not wanting to pay the cost of a throw/catch in order to handle it. On top of that, the exception really isn't being observed, so it's arguably also more correct to not treat the exception as observed. |
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. |
@stephentoub some time ago I had a long thought about what to do with a faulted Regarding the effect of the |
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 So, if we just expose this support for Thus my proposal is simply this: namespace System.Threading.Tasks;
public class Task
{
+ public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitOptions options);
}
+[Flags]
+public enum ConfigureAwaitOptions
+{
/// <summary>No options specified.</summary>
/// <remarks>
/// This behaves identically to using <see cref="Task.ConfigureAwait(bool)"/> with a <see langword="false"/> argument.
/// </remarks>
+ None = 0,
/// <summary>
/// Attempt to marshal the continuation back to the original <see cref="SynchronizationContext"/> or
/// <see cref="TaskScheduler"/> present at the time of the await.
/// </summary>
/// <remarks>
/// If there is no such context/scheduler, or if this option is not specified, the thread on
/// which the continuation is invoked is unspecified and left up to the determination of the system.
/// </remarks>
+ ContinueOnCapturedContext = 0x1,
/// <summary>
/// Avoids throwing an exception at the completion of an await on a <see cref="Task"/> that ends
/// in the <see cref="TaskStatus.Faulted"/> or <see cref="TaskStatus.Canceled"/> state.
/// </summary>
+ SuppressThrowing = 0x2,
/// <summary>
/// Forces an await on an already completed <see cref="Task"/> to behave as if the <see cref="Task"/>"/>
/// wasn't yet completed, such that the current asynchronous method will be forced to yield its execution.
/// </summary>
+ ForceYielding = 0x4,
/// <summary>
/// Forces any continuation created for this await to be queued asynchronously as part of the
/// antecedent <see cref="Task"/>'s completion.
/// </summary>
/// <remarks>
/// This flag is only relevant when awaiting a <see cref="Task"/> that hasn't yet completed,
/// as it impacts how the continuation is stored in the <see cref="Task"/>. When the <see cref="Task"/>
/// eventually completes, if the <see cref="Task"/>'s completion would have otherwise synchronously
/// invoked the continuation as part of its completion routine, this flag will instead force it
/// to be queued for asynchronous execution. This is a consumption-side equivalent to the
/// <see cref="TaskCreationOptions.RunContinuationsAsynchronously"/> flag, which allows the producer
/// of a <see cref="Task"/> to specify that all of that <see cref="Task"/>'s continuations must be
/// queued rather than allowing any to be synchronously invoked.
/// </remarks>
+ ForceAsynchronousContinuation = 0x8,
+} Notes:
Examples: // Wait for a task to complete or a timeout, then see whether the task was successful
await task.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(ConfigureAwaitOptions.SuppressExceptions);
if (task.IsCompleted) { ... } else { ... }
// Queue the remainder of the method to the thread pool
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceAsynchronousCompletion); |
I can't think (off the top of my head) of any other place where we have a For For I don't have a better name, so I'm unfortunately just doing the "I'm not sure this is good... please solve it." It might be the best answer and we're OK with it... I'm just sharing my usability/confusion reaction before this comes up in a meeting. |
We have multiple places where overloads return different types, such that changing arguments changes the return type and this can make it no longer assignable to what it was previously. In this case I'm seeing it as a benefit. |
In discussion we feel that it's valid to add this to We may consider the ValueTask family in later releases. We should also provide an analyzer that detects the call to namespace System.Threading.Tasks;
public partial class Task
{
public ConfiguredTaskAwaitable ConfigureAwait(ConfigureAwaitOptions options);
}
public partial class Task<TResult>
{
// throws if ConfigureAwaitOptions.SuppressExceptions is asserted (or any unknown value, of course)
public new ConfiguredTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitOptions options);
}
[Flags]
public enum ConfigureAwaitOptions
{
None = 0,
ContinueOnCapturedContext = 0x1,
SuppressExceptions = 0x2,
ForceYielding = 0x4,
ForceAsynchronousContinuation = 0x8,
} |
@bartonjs Is |
Largely I wrote down ArgumentException because it's what @stephentoub said. But, off the top of my head, of all the Exception types, ArgumentOutOfRangeException seems the most appropriate: "The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method." NotSupportedException says "The exception that is thrown when an invoked method is not supported, or when there is an attempt to read, seek, or write to a stream that does not support the invoked functionality." The "or when..." is really "we should have used InvalidOperationException on those Stream members, but didn't, so let's backform this and say it's OK". So, NSE is "the method, in its entirety, isn't supported... it probably got here via an interface (or legacy netfx compat)". The derived PlatformNotSupportedException would make contextual sense if the flag worked on some OSes but not all... So, really, yeah, it's just AOORE, or some other ArgumentException, because it's "this method doesn't support this argument value, go fix your calling code" |
Fixed in #87067 |
@stephentoub Thank you! I will finally be able to Task.Delay safely! |
You're welcome. By "safely", I assume you mean you were passing it a cancellation token and were having to try/catch(OperationCanceledException) and now you don't have to? |
I'm planning to submit a pr to aspnetcore once it moves to a new enough runtime. |
Yep!
|
EDITED 5/24/2023 by @stephentoub:
Latest proposal at #22144 (comment)
(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 #22144 (comment) for an up-to-date API proposal.
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:but that incurs the cost of the throw and also triggers first-chance exception handling. You can use a continuation:
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:
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 bothTask
andTask<T>
. Whereas the current overload accepts abool
, the new overload would accept a newConfigureAwaitBehavior
enum:Then with
ConfigureAwait
overloads:code that wants to await without throwing can write:
or that wants to have the equivalent of
ConfigureAwait(false)
and also not throw: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
toTask
, either as an instance or as an extension method, e.g.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:
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.
The text was updated successfully, but these errors were encountered: