Skip to content

Developers can have access to more options when configuring async awaitables #47525

Closed
@eiriktsarpalis

Description

@eiriktsarpalis

Background and Motivation

While it is currently possible to configure task awaitables via the [Value]Task.ConfigureAwait methods, the current API only exposes one setting: continueOnCapturedContext. We have identified a number of other await settings that could be useful to users:

  • Accepting a cancellation token.
  • Awaiting on failing tasks without throwing an exception.
  • Forcing asynchronous execution of continuations.
  • Allowing for future extensions in configurability.

This proposal combines the feature requests from #27723 and #22144.

Usage examples

At the core of the proposal is introducing the following struct in System.Threading.Tasks:

public readonly struct AwaitBehavior
{
    public CancellationToken CancellationToken { get; init; } // = default;
    public TimeSpan Timeout { get; init; } // = Timeout.InfiniteTimeSpan;

    public bool ContinueOnCapturedContext { get; init; } // = true;
    public bool ForceAsync { get; init; } // = false;
    public bool SuppressExceptions { get; init; } // = false;
}

and exposing Task/ValueTask ConfigureAwait overloads that accept it like so:

var result = await task.ConfigureAwait(new AwaitBehavior { ContinueOnCapturedContext = false }); // equivalent to task.ConfigureAwait(false);

Suppressing exceptions

ValueTask<string> failedTask = ValueTask.FromException<string>(new Exception());
string result = await failedTask.ConfigureAwait(new AwaitBehavior { SuppressExceptions = true });
Assert.Equal(null, result);

Cancellation

await task.ConfigureAwait(new AwaitBehavior { CancellationToken = new CancellationToken(true) }); // throws TaskCanceledException

Timeouts

await task.ConfigureAwait(new AwaitBehavior { Timeout = TimeSpan.FromSeconds(1) }); // throws TimeoutException

Note that timeouts are declarative on awaitables; a timer will only be allocated whenever an awaiter instance is created, so we should expect the following behaviour:

var task = Task.Delay(10_000);
var awaitableWithTimeout = task.ConfigureAwait(new AwaitBehavior { Timeout = TimeSpan.FromSeconds(1) });
try { await awaitableWithTimeout ; } catch { } // throws TimeoutException
await task;
await awaitableWithTimeout; // does not throw

Finally, it should be possible to combine all the above settings:

string result = await task.ConfigureAwait(
    new AwaitBehavior 
    { 
        ContinueOnCapturedContext = false,
        SuppressExceptions = true,
        Timeout = TimeSpan.FromSeconds(5), 
        CancellationToken = new CancellationToken(true),
    });

Proposed API

namespace System.Threading.Tasks
{
    public readonly struct AwaitBehavior
    {
        public CancellationToken CancellationToken { get; init; } // = default;
        public TimeSpan Timeout { get; init; } // = Timeout.InfiniteTimeSpan;

        public bool ContinueOnCapturedContext { get; init; } // = true;
        public bool ForceAsync { get; init; } // = false;
        public bool SuppressExceptions { get; init; } // = false;
    }

    public partial class Task
    {
        public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
    }

    public partial class Task<TResult>
    {
        public new System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
    }

    public partial struct ValueTask
    {
        public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
    }

    public partial struct ValueTask<TResult>
    {
        public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult> ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
    }
}

namespace System.Runtime.CompilerServices
{
    public readonly struct ConfiguredCancelableTaskAwaitable
    {
        public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable.ConfiguredCancelableTaskAwaiter GetAwaiter() { throw null; }
        public readonly struct ConfiguredCancelableTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
        {
            public bool IsCompleted { get { throw null; } }
            public void GetResult() { }
            public void OnCompleted(System.Action continuation) { }
            public void UnsafeOnCompleted(System.Action continuation) { }
        }
    }

    public readonly struct ConfiguredCancelableTaskAwaitable<TResult>
    {
        public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult>.ConfiguredCancelableTaskAwaiter GetAwaiter() { throw null; }
        public readonly partial struct ConfiguredCancelableTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
        {
            public bool IsCompleted { get { throw null; } }
            public TResult GetResult() { throw null; }
            public void OnCompleted(System.Action continuation) { }
            public void UnsafeOnCompleted(System.Action continuation) { }
        }
    }

    public readonly partial struct ConfiguredCancelableValueTaskAwaitable
    {
        public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable.ConfiguredCancelableValueTaskAwaiter GetAwaiter() { throw null; }
        public readonly partial struct ConfiguredCancelableValueTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
        {
            public bool IsCompleted { get { throw null; } }
            public void GetResult() { }
            public void OnCompleted(System.Action continuation) { }
            public void UnsafeOnCompleted(System.Action continuation) { }
        }
    }

    public readonly partial struct ConfiguredCancelableValueTaskAwaitable<TResult>
    {
        public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult>.ConfiguredCancelableValueTaskAwaiter GetAwaiter() { throw null; }
        public readonly partial struct ConfiguredCancelableValueTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
        {
            public bool IsCompleted { get { throw null; } }
            public TResult GetResult() { throw null; }
            public void OnCompleted(System.Action continuation) { }
            public void UnsafeOnCompleted(System.Action continuation) { }
        }
    }
}

Implementation

See this commit for a prototype implementation.

Open Questions

Metadata

Metadata

Assignees

Labels

Bottom Up WorkNot part of a theme, epic, or user storyCost:MWork that requires one engineer up to 2 weeksPriority:3Work that is nice to haveTeam:LibrariesUser StoryA single user-facing feature. Can be grouped under an epic.api-approvedAPI was approved in API review, it can be implementedarea-System.Threading.Tasks

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions