[C#] await using で ConfigureAwait() する (構造体でのボクシング回避もしたい)

2022-07-18 (月)

await usingDisposeAsync() を確実に実行できますが、DisposeAsync().ConfigureAwait(false) を使用したい場合は工夫が必要です。
struct においては IAsyncDisposable へのキャストによるボクシングを回避して実行する方法を考えます。

環境

  • .NET 6.0.301
  • C# 10.0
  • Visual Studio 2022 Version 17.2.5
  • Windows 10 Pro 64bit 21H1 19043.1766

使用方法

await using を使用

var myObject = new MyAsyncDisposable();
await using (myObject.ConfigureAwait(false))
{
    // 処理...
}

または

var myObject = new MyAsyncDisposable();
await using var disposable = myObject.ConfigureAwait(false);
// 処理...

ConfigureAwait(false) の部分は myObject

try finally を使用

await using を使用せず try-finally を使用する場合は、特に気にする必要はないと思います。

var myObject = new MyAsyncDisposable();
try
{
    // 処理...
}
finally
{
    await myObject.DisposeAsync().ConfigureAwait(false);
}

実装

struct でボクシング回避して、ConfigureAwait(false)await using する際に使用した実装例です。

internal static class TaskAsyncExtensions
{
    public static ConfiguredAsyncDisposable<T> ConfigureAwait<T>(this T source, bool continueOnCapturedContext) where T : struct, IAsyncDisposable
    {
        return new ConfiguredAsyncDisposable<T>(source, continueOnCapturedContext);
    }
}

internal readonly struct ConfiguredAsyncDisposable<T> where T : struct, IAsyncDisposable
{
    private readonly T _source;
    private readonly bool _continueOnCapturedContext;

    public ConfiguredAsyncDisposable(T source, bool continueOnCapturedContext)
    {
        _source = source;
        _continueOnCapturedContext = continueOnCapturedContext;
    }

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

検証メモ

SharpLab

LINQPad

async Task Main()
{
    var value = new MyDisposableStruct();
    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'");
}

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

public static class TaskAsyncExtensions
{
    public static ConfiguredAsyncDisposable<T> ConfigureAwait<T>(this T source, bool continueOnCapturedContext) where T : struct, IAsyncDisposable
    {
        return new ConfiguredAsyncDisposable<T>(source, continueOnCapturedContext);
    }
}

public readonly struct ConfiguredAsyncDisposable<T> where T : struct, IAsyncDisposable
{
    private readonly T _source;
    private readonly bool _continueOnCapturedContext;

    public ConfiguredAsyncDisposable(T source, bool continueOnCapturedContext)
    {
        _source = source;
        _continueOnCapturedContext = continueOnCapturedContext;
    }

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

感謝

2022-07-18 (月)