ConfigureAwait(false) 和 IAsyncDisposable 的结构实现

ConfigureAwait(false) and struct implementation of IAsyncDisposable

我已经使用 ActionOnAsyncDispose 结构实现了 IAsyncDisposable,如下所示。我的理解是编译器在async using statement:

的时候是不会装箱的
ActionOnDisposeAsync x = ...;
await using (x) {
     ...
}

对吗?到目前为止,一切都很好。我的问题是,当我像这样配置 await 时:

ActionOnDisposeAsync x = ...;
await using (x.ConfigureAwait()) {
     ...
}

x会装箱吗?如果我将 ConfigureAwait 放入方法 Caf():

中会怎么样
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static public ConfiguredAsyncDisposable Caf(this ActionOnDisposeAsync disposable)
    => disposable.ConfigureAwait(false);

ActionOnDisposeAsync x = ...;
await using (x.Caf()) {
     ...
}

在那种情况下我可以避免装箱吗?我无法找到关于我的 using 变量究竟需要实现什么才能获得 ConfigureAwait 效果的文档。似乎也没有任何 public 构造 ConfiguredAsyncDisposable 的方法。

这是 ActionOnDisposeAsync:

public readonly struct ActionOnDisposeAsync : IAsyncDisposable, IEquatable<ActionOnDisposeAsync>
{
    public ActionOnDisposeAsync(Func<Task> actionAsync)
    {
        this.ActionAsync = actionAsync;
    }
    public ActionOnDisposeAsync( Action actionSync)
    {
        this.ActionAsync = () => { actionSync(); return Task.CompletedTask; };
    }
    private Func<Task> ActionAsync { get; }

    public async ValueTask DisposeAsync()
    {
        if (this.ActionAsync != null) {
            await this.ActionAsync();
        }
    }

    ...
}

如果编译器能够检测到实际类型(您的结构),则不需要装箱。如果它仅通过接口工作,它将在处理时使用。我用 ILSpy 之类的东西检查你的编译代码,你会看到 dispose 语句是在 class(对于接口也是这种情况)还是在值类型(/struct)上完成的。

我不确定在处理异步时使用结构是否会给你带来很多好处,以及它是否值得付出努力,但你应该在决定之前衡量一下。

是的,struct 一次性用品上的 ConfigureAwait 导致拳击。这是此行为的实验演示:

MyDisposableStruct value = new();
const int loops = 1000;
var mem0 = GC.GetTotalAllocatedBytes(true);
for (int i = 0; i < loops; i++)
{
    await using (value.ConfigureAwait(false)) { }
}
var mem1 = GC.GetTotalAllocatedBytes(true);
Console.WriteLine($"Allocated: {(mem1 - mem0) / loops:#,0} bytes per 'await using'");

...其中 MyDisposableStruct 是这个简单的结构:

readonly struct MyDisposableStruct : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

输出:

Allocated: 24 bytes per 'await using'

Live demo.

为了防止装箱的发生,您必须创建一个自定义的类似 ConfiguredAsyncDisposable 的结构,它是专门为您的结构量身定制的。这是如何完成的:

readonly struct MyConfiguredAsyncDisposable
{
    private readonly MyDisposableStruct _parent;
    private readonly bool _continueOnCapturedContext;

    public MyConfiguredAsyncDisposable(MyDisposableStruct parent,
        bool continueOnCapturedContext)
    {
        _parent = parent;
        _continueOnCapturedContext = continueOnCapturedContext;
    }

    public ConfiguredValueTaskAwaitable DisposeAsync()
        => _parent.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}

static MyConfiguredAsyncDisposable ConfigureAwait(
    this MyDisposableStruct source, bool continueOnCapturedContext)
{
    return new MyConfiguredAsyncDisposable(source, continueOnCapturedContext);
}

现在 运行 与以前相同的实验,在不对代码进行任何更改的情况下,不会导致分配。输出为:

Allocated: 0 bytes per 'await using'

Live demo.