ConfigureAwaitOptions in .NET 8

ConfigureAwaitOptions

in .NET 8

Intro

In .NET, we can set ConfigureAwait(false) for Task operations to avoid returning to the original synchronization context after the asynchronous operation is completed. .NET 8 introduced a ConfigureAwaitOptions The usage of ConfigureAwait has been further expanded. Let’s take a look at how to use it.

Definition

ConfigureAwaitOptions is a flag enumeration, defined as follows:

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 is equivalent to ConfigureAwait(false)

  • ContinueOnCapturedContext is equivalent to ConfigureAwait(true)

  • SuppressThrowing will not throw an exception in the case of task cancel or exception. This is only valid for Task, for Task is invalid

  • ForceYielding forces yeild even if the task has been 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}");
}

The output is as follows:

b4c39c1eadfa50070494eca0afc13d1b.png

force-yield-output

You can see that when ConfigureAwait is not used, the actual thread does not change, but after adding .ConfigureAwait(ConfigureAwaitOptions.ForceYielding), the subsequent threads have actually changed.

For the completed Task, the asynchronous state machine will not be generated by default. The two will actually be executed on the same thread. .ConfigureAwait(ConfigureAwaitOptions.ForceYielding) An asynchronous state machine will still be generated, not necessarily the same thread before and after

This can be considered using Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding) when we want to use Task.Yield

Task.Yield is equivalent to using Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext | ConfigureAwaitOptions.ForceYielding)

SuppressThrowing sample

Sometimes we will await a task, but an error in the execution of this task will not affect the main process, similar to the following code

try
{
    await task().ConfigureAwait(false);
}
catch
{
    // ignore
}

For such code, we can use the SuppressThrowing option to simplify the code, which can be directly transformed into await task().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing), without the need for us to display it again. Try...catch style

Let’s see an example:

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

What will be the output? The output is as follows:

b72c4aa29b06bfe0fb49b46a916431df.png

supress-throwing-output

You can see that after adding .ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing) when task cancel and task exception occur, the exception will be swallowed and will not affect the execution of the main process.

We mentioned earlier that Task with a return value will not take effect. If it is used against Task, there will be a 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");
}

The output is as follows:

6176fe76a8c793475deacb3a3a02a524.png

When actually used, an error will be reported at runtime.

More

Using the new ConfigureAwaitOptions can simplify some existing code. You can refer to the PR that introduced this feature https://github.com/dotnet/runtime/pull/87067

And a PR to improve hosting based on this 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

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Algorithm skill tree Home page Overview 57287 people are learning the system

syntaxbug.com © 2021 All Rights Reserved.