ConfigureAwait(false)和IAsyncDisposable的结构实现

3

我已经使用ActionOnAsyncDispose结构实现了IAsyncDisposable。我的理解是,在async using语句中,编译器不会将其装箱:

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

没错?目前为止还好。我的问题是,当我这样配置它的时候

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效果的文档。似乎也没有任何公共的方式来构建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();
        }
    }

    ...
}

只有编译器可以静态验证对象类型时才能避免装箱。ConfiguredAsyncDisposable已经被静态确定为结构体,所以不应该有问题。当变量声明类型为IAsyncDisposable时,情况并非如此,因为运行时类型不是静态已知的。 - Charlieface
2个回答

4

是的,对于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'

现场演示

为了防止出现 boxing,您需要创建一个定制的类似于 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'

演示实况


0

如果编译器能够检测到实际类型(您的结构体),则不需要装箱。 如果它只通过接口工作,则在处理时会需要。 通过类似于ILSpy的工具检查您的编译代码,您将看到dispose语句是针对类(也适用于接口)还是针对值类型(/struct)执行。

我不确定在异步处理时使用结构体是否会带来很大收益,以及是否值得这样做,但在决定之前应该进行测量。


重新陈述我的问题,假设我想要实现自己的结构体来替换 ConfiguredAsyncDisposable。为了让编译器在 await using 语句中识别出我的结构体,它需要实现哪些方法? - sjb-sjb
在你的例子中,编译器会知道,在处理你的结构时,而不是任何在运行时未知的I(Async)Disopasable。对于像IEnumerators这样的东西也是如此,尽管这是完全不同的话题。 - Corniel Nobel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接