.NET 8 中的 ConfigureAwaitOptions

.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}");
}

输出结果如下:

b4c39c1eadfa50070494eca0afc13d1b.png

force-yield-output

可以看到不使用 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");

会输出什么结果呢?输出结果如下:

b72c4aa29b06bfe0fb49b46a916431df.png

supress-throwing-output

可以看到在 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");
}

输出结果如下:

6176fe76a8c793475deacb3a3a02a524.png

实际使用的时候在运行时也会报错

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值