dotnet / dotnet/csharplang

Proposal: Allow [AsyncMethodBuilder(...)] on methods

Open
#1,407 36 comments 66 reactions 0 assignees View on GitHub
Implemented Needs ECMA Spec Proposal Proposal champion
Dominant language
C#
Stars
12.7k
Forks
1.1k
Avg merge
11h 1m
Merged PRs (30d)
3

Description

EDIT: Proposal added 11/5/2020:
https://github.com/dotnet/csharplang/blob/master/proposals/csharp-10.0/async-method-builders.md

### Background

AsyncMethodBuilderAttribute can be put on a type to be used as the return type of an async method, e.g.
```C#
public struct AsyncCoolTypeMethodBuilder
{
public static AsyncCoolTypeMethodBuilder Create();
...
}

[AsyncMethodBuilder(typeof(AsyncCoolTypeMethodBuilder))]
public struct CoolType { ... }

public async CoolType SomeMethodAsync() { ... } // will implicitly use AsyncCoolTypeMethodBuilder.Create()
```

This, however, means that:
- every use of a given return type requires the same builder.
- there's no context available to be passed to the builder.
- if you don't own the type and it's not already attributed, you can't use it as an async return type.

### Proposal

Two parts:

1. Allow AsyncMethodBuilderAttribute to be applied to methods. When applied to an async method, it would be used as the builder for that method, overriding any AsyncMethodBuilderAttribute on the return type.
2. Allow the builder's Create method to have arguments. Specifically, it can have arguments that match the parameters to the method to which it's applied, including the implicit `this` for instance methods. The compiler will then forward the arguments to the method's invocation into the builder's Create.

#### Ammortized allocation-free async methods

There are four types in .NET with built-in builders: `Task`, `ValueTask`, `Task`, and `ValueTask`. In .NET Core, significant work has gone in to optimizing async methods with these types, and thus in the majority of uses, each async method invocation will incur at most one allocation of overhead, e.g. a synchronously completing async method returning `ValueTask` won’t have any additional allocations, and an asynchronously completing async method returning `ValueTask` will incur one allocation for the underlying state machine object.

However, with this feature, it would be possible to avoid even that allocation, for developers/scenarios where it really mattered. .NET Core 2.1 sees the introduction of `IValueTaskSource` and `IValueTaskSource`. Previously `ValueTask` could be constructed from a `T` or a `Task`; now it can also be constructed from an `IValueTaskSource`. That means a `ValueTask` can be wrapped around an arbitrary backing implementation, and that backing implementation can be reused or pooled (.NET Core takes advantage of this in a variety of places, for example enabling allocation-free async sends and receives on sockets). However, there is no way currently for an `async ValueTask` method to utilize a custom `IValueTaskSource` to back it, because the builder can only be assigned by the developer of the `ValueTask` type; thus it can’t be customized for other uses.

If a developer could write:
```C#
[AsyncMethodBuilder(UseMyCustomValueTaskSourceMethodBuilder)]
public async ValueTask SomeMethodAsync() { … }
```
then the developer could write their own builder that used their own `IValueTaskSource` under the covers, enabling them to plug in arbitrary logic and even to pool.

However, such a pool would end up being shared across all uses of SomeMethodAsync. This could be a significant scalability bottleneck. If a pool for this were being managed manually, a developer would be able to make the pool specific to a particular instance rather than shared globally. For example, `WebSocket`’s `ValueTask<…> ReceiveAsync(…)` method utilizing this feature would end up hitting a pool shared by all `WebSocket` instances for this particular WebSocket-derived type, but given `WebSocket`’s constraints that only one receive can be in flight at a time, `WebSocket` can have a very efficient “pool” of a single `IValueTaskSource` that’s reused over and over. To enable that, the compiler could pass the `this` into `UseMyCustomValueTaskSourceMethodBuilder`’s `Create` method, e.g.
```C#
internal sealed class ManagedWebSocket : WebSocket
{
private struct WebSocketReceiveMethodBuilder
{
private ManagedWebSocket _webSocket;

public static WebSocketReceiveMethodBuilder Create(ManagedWebSocket thisRef, Memory buffer, CancellationToken cancellationToken) =>
new WebSocketReceiveMethodBuilder { _webSocket = thisRef };


}

private IValueTaskSource _receiveVtsSingleton;

[AsyncMethodBuilder(typeof(WebSocketReceiveMethodBuilder)]
public override async ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken) { … }
}
```
The ReceiveAsync implementation can then be written using awaits, and the builder can use `_webSocket._receiveVtsSingleton` as the cache for the single instance it creates. As is now done in .NET Core for task’s builder, it can create an `IValueTaskSource` that stores the state machine onto it as a strongly-typed property, avoiding a separate boxing allocation for it. Thus all state for the async operation becomes reusable across multiple ReceiveAsync invocations, resulting in amortized allocation-free calls.

### Related

https://github.com/dotnet/corefx/issues/27445
https://github.com/dotnet/coreclr/pull/16618
https://github.com/dotnet/corefx/pull/27497

### LDM Notes
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-29.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2020/LDM-2020-11-11.md

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading the linked C# 10.0 proposal, async-method-builders.md, then review the listed LDM notes from 2020-11-11 and 2021-03-29. Check the related CoreFX and CoreCLR issues for context. Done means the language-design questions are resolved and the proposal is accepted or otherwise given a clear decision.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.