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

xin9le.net

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

非同期メソッド単位で値を保持/提供するAsyncLocal<T>

2015/07/20 深夜、ついに Visual Studio 2015 が正式リリースされました。同時に .NET Framework も 4.6 にバージョンアップ。以下の記事にもある通り、関連の言語やライブラリも数多く更新されています。

Announcing .NET Framework 4.6
.NET 4.6 (@matarillo さんによる日本語まとめ)

非同期処理関連もいくつか機能追加がされていて、ひとつは Task 型の便利メソッドたち。だいぶ前にまとめたものですが、以下をご参照いただければと思います。.NET 4.5.3 と書いてあるところは .NET 4.6 と読み替えていただければ。

もうひとつは今回紹介する AsyncLocal<T> クラスです。

Stack 型のデータストア

AsyncLocal<T> ってなんぞや?簡単に言うと、非同期メソッド (async キーワード) 単位で値を保持/提供してくれるスコープ依存の Stack 型データストアです。何言ってるかよく分からないと思うので、サンプルコードで挙動を確認してみましょう。

static async Task TestAsync()
{
    //--- インスタンス生成
    var x = new AsyncLocal<int>();

    //--- 現在の値を確認しつつ、値を書き換える
    Console.WriteLine($"Step 0. {x.Value}");
    x.Value = 1;
    Console.WriteLine($"Step 1. {x.Value}");

    //--- 非同期処理(1)を実行
    await Task.Run(async () =>
    {
        //--- 値確認 & 書き換え
        Console.WriteLine($"Step 2. {x.Value}");
        x.Value = 2;
        Console.WriteLine($"Step 3. {x.Value}");

        //--- 非同期処理(2)を実行
        await Task.Run(async () =>
        {
            //--- 値確認 & 書き換え
            Console.WriteLine($"Step 4. {x.Value}");
            x.Value = 3;
            Console.WriteLine($"Step 5. {x.Value}");

            //--- 非同期処理(3)を実行
            await Task.Delay(1);

            //--- 値確認 & 書き換え
            Console.WriteLine($"Step 6. {x.Value}");
            x.Value = 4;
            Console.WriteLine($"Step 7. {x.Value}");
        });

        //--- 値確認 (注目!!)
        Console.WriteLine($"Step 8. {x.Value}");
    });

    //--- 値確認 (注目!!)
    Console.WriteLine($"Step 9. {x.Value}");
}

/*
Step 0. 0
Step 1. 1
Step 2. 1
Step 3. 2
Step 4. 2
Step 5. 3
Step 6. 3
Step 7. 4
Step 8. 2  //--- 自動的に値が復元される
Step 9. 1  //--- 自動的に値が復元される
*/

AsyncLocal<T> のインスタンス x の Value プロパティを入れ子になった非同期メソッドの中で上書きしているにも関わらず、その入れ子の非同期処理を抜けて元のスコープに戻ると x.Value の値も元に戻っている (Step.8 / Step.9) のが確認できると思います。要点は大体以下のような感じです。

  • 非同期メソッド (async キーワード) のスコープ単位で値を保持できる (push)
  • スコープ内での値を設定するとその値を参照できる (peek)
  • スコープ内で値を設定しなければ親スコープの値が取得できる (peek)
  • 一旦自スコープ内での値を設定すると、その後親スコープの値は参照できなくなる
  • スコープを抜けて元のスコープに戻ると、そのスコープでの値が参照できるようになる (pop)

async キーワード単位での Stack 構造であることがわかったでしょうか。

コンテキストの変更をフック

先の例では AsyncLocal<T> のコンストラクタに引数を与えていませんでしたが、コールバック用のデリゲートを渡すオーバーロードも提供されています。このデリゲートは Value プロパティに値が入れられたり、async コンテキスト変更による現在値の変更が発生した際に呼び出されます。先のコードはそのままに、コンストラクタにデリゲートを与えてみましょう。

static async Task Test()
{
    var x = new AsyncLocal<int>(args =>
    {
        Console.WriteLine($"callback : previous = {args.PreviousValue} | current = {args.CurrentValue} | contextChanged = {args.ThreadContextChanged}");
    });

    //--- 以下略
}


/*
Step 0. 0
callback : previous = 0 | current = 1 | contextChanged = False
Step 1. 1
callback : previous = 0 | current = 1 | contextChanged = True
callback : previous = 1 | current = 0 | contextChanged = True
Step 2. 1
callback : previous = 1 | current = 2 | contextChanged = False
Step 3. 2
callback : previous = 2 | current = 1 | contextChanged = True
callback : previous = 1 | current = 0 | contextChanged = True
callback : previous = 0 | current = 2 | contextChanged = True
Step 4. 2
callback : previous = 2 | current = 3 | contextChanged = False
Step 5. 3
callback : previous = 3 | current = 2 | contextChanged = True
callback : previous = 2 | current = 0 | contextChanged = True
callback : previous = 0 | current = 3 | contextChanged = True
Step 6. 3
callback : previous = 3 | current = 4 | contextChanged = False
Step 7. 4
callback : previous = 4 | current = 2 | contextChanged = True
Step 8. 2
callback : previous = 2 | current = 1 | contextChanged = True
Step 9. 1
callback : previous = 1 | current = 2 | contextChanged = True
callback : previous = 2 | current = 4 | contextChanged = True
*/

途中不思議な動きをしていますが、そういう風に動いたんでしょう...。使いどころ/利用シーンは...浮かんでません。