簡體   English   中英

ConfigureAwait(false) 和 IAsyncDisposable 的結構實現

[英]ConfigureAwait(false) and struct implementation of IAsyncDisposable

我已經使用 ActionOnAsyncDispose 結構實現了 IAsyncDisposable,如下所示。 我的理解是,當它處於異步 using 語句中時,編譯器不會將其裝箱:

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 效果的文檔。 似乎也沒有任何構建 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();
        }
    }

    ...
}

是的, struct disposables 上的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;
}

Output:

Allocated: 24 bytes per 'await using'

現場演示

為了防止裝箱發生,您必須創建一個自定義的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);
}

現在運行與以前相同的實驗,不對代碼進行任何更改,不會導致分配。 output 是:

Allocated: 0 bytes per 'await using'

現場演示

如果編譯器能夠檢測到實際類型(您的結構),則不需要裝箱。 如果它僅通過接口工作,它將在處理時使用。 我用 ILSpy 之類的東西檢查你的編譯代碼,你會看到 dispose 語句是在 class 上完成的(對於接口也是如此),還是在值類型 (/struct) 上完成。

我不確定在處理異步時使用結構是否會給你帶來很多好處,以及是否值得付出努力,但你應該在決定之前衡量一下。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM