2022-07-18 (月)
[C#] await using で ConfigureAwait() する (構造体でのボクシング回避もしたい)
await using で DisposeAsync() を確実に実行できますが、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 が
classの場合、TaskAsyncEnumerableExtensions.ConfigureAwait(this IAsyncDisposable, bool) が呼ばれます。structの場合、下記実装の TaskAsyncExtensions.ConfigureAwait<T>(this T, bool) where T : struct, IAsyncDisposable が呼ばれます。
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);
}
検証メモ
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);
}
感謝
関連記事
新着記事