読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

IAsyncDisposableについて考えてみた

Visual Studio 2015 も RC (= リリース候補版) となり、C# 6.0 も Go-Live となりました。言語チームは Roslyn という大物の開発に注力していたため、C# 6.0 は細かな使い勝手の向上を中心とした比較的小規模な機能追加となっています。詳細は以下をご覧いただければ幸いです。

そんな C# 6.0 の新機能には 'catch/finally 句での await' が追加されています。これまでは finally 句に await を書くことができませんでしたが、この制限緩和によって try - finally に展開される using 句も await できて良いのではないか。そう考えて、ちょこっとサンプルを書いてみました。

IAsyncDisposable インターフェース

インターフェースは以下のような感じで 1 つのメソッドと 1 つのプロパティを持つようにします。非同期実行での Dispose 処理と、Dispose 後に実行コンテキストを呼び出し元に戻すかどうかを決定します。

public interface IAsyncDisposable : IDisposable  //--- 既存との互換のため継承
{
    Task DisposeAsync();  //--- 非同期的なリソースの解放
    bool ContinueOnCapturedContext { get; }  //--- 非同期処理後に実行コンテキストを呼び出し元に戻すかどうか
}

実装例と利用例

上で定義した IAsyncDisposable インターフェースを実装します。今は特に何もしないで終了する実装になっていますが、その点はご了承ください。Dispose メソッドは DisposeAsync を待機するようにだけしておきます。

public class AsyncDisposableObject : IAsyncDisposable
{
    #region インターフェース実装
    public void Dispose() => this.DisposeAsync().Wait();  //--- 既定の実装は非同期の待機
    public Task DisposeAsync() => Task.CompletedTask;     //--- リソース解放 (メイン処理)
    public bool ContinueOnCapturedContext { get; } = true;
    #endregion

    public Task DoSomethingAsync() => Task.CompletedTask;  //--- 何か非同期処理
}

利用するときは以下のように書きます。await using は僕が勝手に考えたものなので、当然ながら現在の C# には実装されていません。

async Task SampleAsync()
{
    await using (var obj = new AsyncDisposableObject())
    {
        await obj.DoSomethingAsync();
    }
}

これはコンパイラによって以下のように展開されることを期待しています。await using は非同期メソッド (async キーワードが付いたメソッド) かつ IAsyncDisposable が実装されている場合にのみ書けるようにすることを想定しています。

async Task SampleAsync()
{
    var obj = new AsyncDisposableObject();
    try
    {
        await obj.DoSomethingAsync();
    }
    finally
    {
        if (obj != null)
            await obj.DisposeAsync().ConfigureAwait(obj.ContinueOnCapturedContext);
    }
}

いかがでしょうか。結構スッキリ書けている気がしませんか?

await using

イチイチ await using などと書かなくても using だけでも良いのかもしれませんが、既存の Disposable オブジェクトが突然 IAsyncDisposable を実装すると挙動が変わってしまうので、非同期的に Dispose するよと明示すべきと思い、付けてみました。

問題点

とりあえずで書いてみましたが、実は IAsyncDisposable には大きな問題/課題があります。最も困るのが DisposeAsync の後に実行コンテキストをどうするかというものです。上の例では ContinueOnCapturedContext プロパティを用意してみたものの、この実装如何では (false を返すようにライブラリが実装していたら) 以降 UI 要素を操作できなくなります。では常に ConfigureAwait の引数に true を渡すようにすれば良いのかというと、パフォーマンス上そうしたくない場合もありますし、ライブラリの場合はアプリケーション層でのデッドロックを防ぐために常に false を指定したいです。

このあたりのせめぎあいが大きな課題ではありますが、IAsyncDisposable についてはすでに GitHub で議論されており、(どのような形になるかはさておき) C# 7.0 での搭載が望まれます。