-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
EDITED 5/24/2023 by @stephentoub:
Latest proposal at #22144 (comment)
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 #22144 (comment) 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.