.NET 8 中的 ConfigureAwaitOptions
Intro
在 .NET 中我们可以针对 Task 的操作来设置 ConfigureAwait(false)
来避免异步操作完成后回到原始的同步上下文,.NET 8 中引入了一个 ConfigureAwaitOptions
进一步扩展了 ConfigureAwait
的用法,我们一起来看下如何使用吧
Definition
ConfigureAwaitOptions
是一个 flag 枚举, 定义如下:
namespace System.Threading.Tasks;
/// <summary>Options to control behavior when awaiting.</summary>
[Flags]
public enum ConfigureAwaitOptions
{
/// <summary>No options specified.</summary>
/// <remarks>
/// <see cref="Task.ConfigureAwait(ConfigureAwaitOptions)"/> with a <see cref="None"/> argument behaves
/// identically to using <see cref="Task.ConfigureAwait(bool)"/> with a <see langword="false"/> argument.
/// </remarks>
None = 0x0,
/// <summary>
/// Attempt to marshal the continuation back to the original <see cref="SynchronizationContext"/> or
/// <see cref="TaskScheduler"/> present on the originating thread 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.
/// <see cref="Task.ConfigureAwait(ConfigureAwaitOptions)"/> with a <see cref="ContinueOnCapturedContext"/> argument
/// behaves identically to using <see cref="Task.ConfigureAwait(bool)"/> with a <see langword="true"/> argument.
/// </remarks>
ContinueOnCapturedContext = 0x1,
/// <summary>
/// Avoids throwing an exception at the completion of awaiting a <see cref="Task"/> that ends
/// in the <see cref="TaskStatus.Faulted"/> or <see cref="TaskStatus.Canceled"/> state.
/// </summary>
/// <remarks>
/// This option is supported only for <see cref="Task.ConfigureAwait(ConfigureAwaitOptions)"/>,
/// not <see cref="Task{TResult}.ConfigureAwait(ConfigureAwaitOptions)"/>, as for a <see cref="Task{TResult}"/> the
/// operation could end up returning an incorrect and/or invalid result. To use with a <see cref="Task{TResult}"/>,
/// cast to the base <see cref="Task"/> type in order to use its <see cref="Task.ConfigureAwait(ConfigureAwaitOptions)"/>.
/// </remarks>
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,
}
None
等同于ConfigureAwait(false)
ContinueOnCapturedContext
等同于ConfigureAwait(true)
SuppressThrowing
会在 task cancel 或者异常的情况下,不抛出异常,这只对Task
有效,针对有返回值的Task<T>
无效ForceYielding
强制yeild
即使 task 已经 completed
Sample
ForceYield
sample
public static async Task ForceYielding()
{
DumpThreadInfo();
await Task.CompletedTask;
DumpThreadInfo();
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
DumpThreadInfo();
}
private static void DumpThreadInfo()
{
Console.WriteLine($"ThreadId: {Environment.CurrentManagedThreadId}");
}
输出结果如下:
可以看到不使用 ConfigureAwait
的时候,实际线程是没有变化的,但是加了 .ConfigureAwait(ConfigureAwaitOptions.ForceYielding)
之后后面的线程实际就已经变掉了
针对于已经完成的 Task
不会默认不会生成异步状态机,前后实际会是在同一个 thread 上执行的,加了 .ConfigureAwait(ConfigureAwaitOptions.ForceYielding)
还是会生成异步状态机,前后不一定是同一个 thread
这在我们想要使用 Task.Yield
的时候可以考虑使用 Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding)
Task.Yield
等同于使用 Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext | ConfigureAwaitOptions.ForceYielding)
SuppressThrowing
sample
在有些时候我们会 await 一个 task,但是这个 task 执行出错了不影响主流程,类似下面这样的代码
try
{
await task().ConfigureAwait(false);
}
catch
{
// ignore
}
针对这样的代码,我们可以使用 SuppressThrowing
这个 option 来简化代码,可以直接变成 await task().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)
,不需要我们再显式的 try...catch 了
来看个示例:
using var cts = new CancellationTokenSource();
cts.CancelAfter(100);
try
{
await Task.Delay(1000, cts.Token);
}
catch (Exception e)
{
Console.WriteLine(e);
}
try
{
await Task.Delay(1000, cts.Token);
}
catch (Exception e)
{
Console.WriteLine(e);
}
var startTimestamp = TimeProvider.System.GetTimestamp();
await Task.Delay(1000, cts.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
Console.WriteLine($"{TimeProvider.System.GetElapsedTime(startTimestamp).TotalMicroseconds} ms");
try
{
await ThrowingTask();
}
catch (Exception e)
{
Console.WriteLine(e);
}
await ThrowingTask().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
Console.WriteLine("Yeah");
会输出什么结果呢?输出结果如下:
可以看到在 task cancel 和 任务异常的时候加了 .ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)
之后,异常会被吞掉,不会影响主流程的执行
前面我们提到了针对有返回值的 Task
是不会生效的,如果针对 Task<T>
使用会有一个 warning
try
{
// CA2261: The ConfigureAwaitOptions.SuppressThrowing is only supported with the non-generic Task
await ThrowingTaskWithReturnValue().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
catch (Exception e)
{
Console.WriteLine(e);
}
private static async Task<int> ThrowingTaskWithReturnValue()
{
await Task.Delay(100);
throw new InvalidOperationException("Balabala2");
}
输出结果如下:
实际使用的时候在运行时也会报错
More
使用新的 ConfigureAwaitOptions
可以简化一些现有的代码,可以参考引入这一特性的 PR https://github.com/dotnet/runtime/pull/87067
以及基于此改进 hosting 的 PR https://github.com/dotnet/runtime/pull/93949
References
https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.configureawaitoptions?view=net-8.0
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ConfigureAwaitOptions.cs
https://github.com/dotnet/runtime/pull/87067
https://github.com/dotnet/runtime/pull/87067/files#diff-55c104dbddd158b7d649d07c342facc10f353b1b036bb35a0ad71625073eb637
https://github.com/dotnet/runtime/pull/93949
https://github.com/WeihanLi/SamplesInPractice/blob/master/net8sample/Net8Sample/ConfigureAwaitOptionsSample.cs