-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Developers can have access to more options when configuring async awaitables #47525
Comments
Tagging subscribers to this area: @tarekgh Issue DetailsBackground and MotivationExisting
This proposal combines the feature requests from #27723 and #22144. Usage examplesAt the core of the proposal is introducing the following enum in [Flags]
public enum AwaitBehavior
{
Default = 0x0,
NoCapturedContext = 0x1,
NoThrow = 0x2,
ForceAsync = 0x4,
} and exposing Task/ValueTask var result = await task.ConfigureAwait(AwaitBehavior.NoCapturedContext); // equivalent to task.ConfigureAwait(false); The string result = await ValueTask.FromException<string>(new Exception()).ConfigureAwait(AwaitBehavior.NoThrow);
Assert.Equal(null, result); Passing cancellation tokens should work like so: await Task.Delay(10_000).ConfigureAwait(new CancellationToken(true)); // throws OperationCanceledException Finally, it should be possible to combine all settings: string result = await task.ConfigureAwait(AwaitBehavior.NoCapturedContext | AwaitBehavior.ForceAsync, cancellationToken); Proposed APInamespace System.Threading.Tasks
{
[Flags]
public enum AwaitBehavior
{
Default = 0x0,
NoCapturedContext = 0x1,
NoThrow = 0x2,
ForceAsync = 0x4,
}
public partial class Task
{
public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable ConfigureAwait(System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable ConfigureAwait(bool continueOnCapturedContext, System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial class Task<TResult>
{
public new System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(System.Threading.CancellationToken cancellationToken) { throw null; }
public new System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext, System.Threading.CancellationToken cancellationToken) { throw null; }
public new System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitBehavior awaitBehavior) { throw null; }
public new System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitBehavior awaitBehavior, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial struct ValueTask
{
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable ConfigureAwait(CancellationToken cancellationToken) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable ConfigureAwait(bool continueOnCapturedContext, CancellationToken cancellationToken) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior, CancellationToken cancellationToken) { throw null; }
}
public partial struct ValueTask<TResult>
{
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult> ConfigureAwait(CancellationToken cancellationToken) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext, CancellationToken cancellationToken) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult> ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult> ConfigureAwait(AwaitBehavior awaitBehavior, CancellationToken cancellationToken) { 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 ConfiguredValueTaskAwaitable
{
public System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter GetAwaiter() { throw null; }
public readonly partial struct ConfiguredValueTaskAwaiter : 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 ConfiguredValueTaskAwaitable<TResult>
{
public System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>.ConfiguredValueTaskAwaiter GetAwaiter() { throw null; }
public readonly partial struct ConfiguredValueTaskAwaiter : 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) { }
}
}
} ImplementationSee this commit for a reference implementation. Open Questions
|
Can there be extension methods for shortcuts of common await options? Or, do some exploration with dotnet/csharplang#2649 . |
I would hope the majority of awaits would not use this support. It's important here and there, but if the majority of awaits need these options, I suspect there are bigger issues to be addressed. |
I'd be happy to have a shortcut for the |
There are many discussions/issues about that over in dotnet/csharplang. |
@eiriktsarpalis, I don't see timeouts represented here. Weren't you going to have some overloads that took a TimeSpan? My concern with relying on CancellationToken in this context is we're likely to see devs accidentally write code like: await task.ConfigureAwait(new CancellationTokenSource(timeout).Token); and while an analyzer could be written to flag that and suggest it instead be: using (var cts = new CancellationTokenSource(timeout))
await task.ConfigureAwait(cts.Token); it's easier to get it right with the timeout overload. The above also makes the already-completed task case much more expensive. I'd be ok if we just wanted to have one overload per-task-like type that takes a timeout in addition to options and a token. Or is there a reason not to do this? |
No real reason. Should we use |
I'd stick with just TimeSpan in the proposal, and call it out as an open issue to discuss in API review in case anyone has a different opinion. |
Also, did you consider alternatives for exposing this? e.g. rather than having a bunch of overloads, you could have one with defaults (though I don't know what we'd do for a TimeSpan default given the type), or potentially a struct with the options: public struct AwaitBehavior
{
public bool ContinueOnCapturedContext { get; set; } // defaults to true
public bool SuppressExceptions { get; set; } // defaults to false
public bool ForceAsynchronous { get; set; } // defaults to false
public CancellationToken CancellationToken { get; set; } // defaults to default
public TimeSpan Timeout { get; set; } // defaults to -1
} etc. Then you'd just have one new overload per task type Just throwing out other approaches you might consider. |
@stephentoub seems like a reasonable alternative, should we consider making this a class to accommodate potential future additions? |
Here's an example of how timeouts could be implemented over the existing proposal. Each new awaiter must allocate a I'm not particularly fond of this approach, and it suggests to me that perhaps awaiters are not the appropriate layer of abstraction for introducing timeouts. Alternatively we could expose this feature as task extension, building on top of cancellable awaiters: public static async ValueTask<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
if (timeout == Timeout.InfiniteTimeSpan)
{
return await task.ConfigureAwait(false);
}
using var cts = new CancellationTokenSource(timeout);
try
{
return await task.ConfigureAwait(false, cts.Token);
}
catch (OperationCanceledException e)
{
throw TimeoutException("blah", e);
}
} |
This is pertinent to @stephentoub's proposal in #47525 (comment), in that it would be expected to support scenaria where both a cancellation token and timeout are specified, which would make the implementation more complicated than needed. |
If we decided to go with a WithTimeout, I don't think we'd build it this way. It'd be better in that case to build it on top of Timer (or TimerQueueTimer) directly, rather than the layer of CancellationTokenSource in the middle, throwing an extra layer of exception, etc. |
Not really. You still need some shared object that can coordinate between the Task, the CancellationToken, and the Timer (e.g. something you can pass to all of their callbacks), that can hold onto the Task to be able to unregister from it, the CancellationTokenRegistraiton to be able to dispose of it, and the Timer to be able to dispose of it, etc. That might as well be in a Task object itself, at which point you can reuse the exact same optimized awaiter and not increase the size of every state machine that awaiter gets baked into (multiplied by every unique awaiter type used in the method). |
Working on some unrelated code I remembered what the biggest use of |
I use a helper for that in every single project I've worked on, frontend or backend. It's not quite trivial to get done right, so it'd be great to have it built-in. E.g., there could be an Or vice versa, I've seen cases where I think, a proper helper for observing cancellations should be taking care of the corner cases like these. Here's what I use, it returns // this will throw, unless the returned task is either `IsCompletedSuccessfully` or `IsCanceled`
var task = await TestAsync(token).IgnoreCancellations();
if (task.IsCanceled) return;
Console.WriteLine(task.Result);
// ...
public static Task<Task<T>> IgnoreCancellations<T>(this Task<T> @this)
{
return @this.ContinueWith<Task<T>>(
continuationFunction: anteTask =>
{
if (anteTask.IsFaulted)
{
if (anteTask.Exception.Flatten().InnerExceptions.All(e => e is OperationCanceledException))
{
// all cancelled, return a cancelled task
return Task<T>.FromCanceled<T>(new CancellationToken(canceled: true));
}
// rethrow
anteTask.GetAwaiter().GetResult();
}
return anteTask;
},
cancellationToken: CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
scheduler: TaskScheduler.Default);
} |
For syntactic sugar, can we please have await AND awaitf keywords (or similar)? That way, all my code that is like:
...can just be:
...or even...
|
Looks like syntax salt 😆 |
The idea of syntax sugar is interesting though, given how pervasive |
@davidnmbond Nah, The best proposal to this I've seen is to allow extending an |
I see some features in proposed APIs I don't get, I'm not going to argue against them. But what I'd like to see is methods that do what they say. I want call sites that look like:
These methods say what they do. "Await this thing with a captured context", not "await this thing but configure the context false". It helps people read the code like they think. I've seen an awful lot of newbies completely flummoxed by |
I really like this proposal, and I can heartily relate to the reasons @SittenSpynne provides behind it. I'd also add
I think this concept would play well with another proposed API, Such a set of new, custom-awaiter-based APIs wouldn't be a breaking change, and wouldn't make |
This would mean a huge explosion of APIs (the awaiter returned by ForceAsync() needs WithoutContext() and IgnoreCancellations(), as well as the awaiter returned by WithoutContext(), as well as the awaiter returned by IgnoreCancellations(), and so on so forth for each API), which is what the original API proposal (ConfigureAwait(AwaitBehaviour)) is supposed to prevent. |
I don't see why they cannot be composable without provisioning for all possible combinations? Each API could create an awaiter which is responsible for one thing (e.g, make it async), and that can be composed with another awaiter (e.g., ignore errors), in any particular order. |
I think this is the key point here. Can they really be combined in any order? If so, can they be implemented as extension methods today, on |
I need to do some experimenting, but I believe that's quite achievable. It should be just about implementing the awaiter protocol methods correctly. The problems with making them extensions of I think I may have a clear picture of how to implement extensions like |
If you want to make them composable without defining, e.g. a |
That's not necessary if the struct contains all the fields for each method, then you just return a copied struct with that field changed. |
But then you're not really satisfying the "responsible for one thing" point you seem to be so keen on. Also, at that point, you could just have a singular API which creates the singular struct with all the fields set anyway, which brings us back to the original API proposal. |
I think you have me confused with someone else, but yeah, a single API with optional parameters makes sense to me. |
That was me. Not saying it's 100% possible, but let's see if I can put some PoC code together (edited: here). |
@FiniteReality here's a gist with a quick proof of concept of what I meant above, where each awaiter is responsible for one specific thing:
Disclaimer: it's almost untested and not optimized to reduce allocations. Other ideas for chain-able custom awaiters like that:
Thinking about extra allocations and the GC burden, this doesn't seem to be a big issue for JavaScript promises in Node.js, the ecosystem that also runs some quite high-performance sites and microservices out there. Here's a good read: https://v8.dev/blog/fast-async |
namespace System.Threading.Tasks
{
public partial class Task
{
public Task WaitAsync(CancellationToken cancellationToken);
public Task WaitAsync(TimeSpan timeout);
public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken);
}
public partial class Task<TResult>
{
public Task<TResult> WaitAsync(CancellationToken cancellationToken);
public Task<TResult> WaitAsync(TimeSpan timeout);
public Task<TResult> WaitAsync(TimeSpan timeout, CancellationToken cancellationToken);
}
} |
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: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
:and exposing Task/ValueTask
ConfigureAwait
overloads that accept it like so:Suppressing exceptions
Cancellation
Timeouts
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:
Finally, it should be possible to combine all the above settings:
Proposed API
Implementation
See this commit for a prototype implementation.
Open Questions
AwaitBehavior
struct? There's already Allow an await to capture the current TaskScheduler instead of the current SynchronizationContext #47433 proposing capturing the current TaskScheduler.ConfiguredCancelable[Value]TaskAwaitable
an appropriate name for the new awaiters?TaskCanceledException
orOperationCanceledException
? The latter might make more sense since we're cancelling an awaiter, and not a task.The text was updated successfully, but these errors were encountered: