5
4

More than 1 year has passed since last update.

C# AsyncLocal の振る舞い

Last updated at Posted at 2018-11-16

今貢献しているプロジェクトで、AsyncLocal のアドバンスドな使い方をする必要があるので、まずはなんとなく使っている AsyncLocal のリファレンスを読んで挙動を確認してみた。

定義を読むと、Asyncのコントロールフロー(例えばAsyncメソッド)の中でローカルとして有効な限定された範囲内でのデータという感じ。読むだけではよくわからないので実験して定義を理解してみる。

Code

内容は、AsyncLocal と ThreadLocal を使って、スレッドの挙動と共に振る舞いを調査しています。

  • ExecuteAsync
  • Level1
  • Level2

とそれぞれ AsyncLocal, ThreadLocal を更新してどのスコープで値がシェアされるかを確認しています。

   class Program
    {

        private static AsyncLocal<string> asyncLocal = new AsyncLocal<string>();
        private static ThreadLocal<string> threadLocal = new ThreadLocal<string>();
        static void Main(string[] args)
        {
            var program = new Program();
            program.ExecuteAsync().GetAwaiter().GetResult();
            Console.ReadLine();
        }

        private async Task ExecuteAsync()
        {
            asyncLocal.Value = "Value 1";
            threadLocal.Value = "Value 1";
            var t1 = Level1("1");
            asyncLocal.Value = "Value 2";
            threadLocal.Value = "Value 2";
            var t2 = Level1("2");

            await t1;
            await t2;
            Console.WriteLine($"Level0: AsyncLocal: {asyncLocal.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"Level0: ThreadLocal: {threadLocal.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");
        }

        private async Task Level1(string value)
        {
            Console.WriteLine($"Level1:Start: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");
            asyncLocal.Value = $"{asyncLocal.Value}.{value}";
            threadLocal.Value = $"{threadLocal.Value}.{value}";
            // await Task.Delay(100);  // If you add wait, then, the it switch the Thread.
            await Level2(value);
            Console.WriteLine($"Level1:End: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");

        }

        private async Task Level2(string value)
        {
            Console.WriteLine($"Level2:Start: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");
            asyncLocal.Value = $"{asyncLocal.Value}.{value}";
            threadLocal.Value = $"{threadLocal.Value}.{value}";
            Console.WriteLine($"Level2:End: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");

        }
    }

実行結果 (Thread 1つ)

実行してみると、AsyncLocal と ThreadLocal で挙動が異なる。AsyncLocal はコールグラフの中で値がシェアされているが、Stack の形式で情報がシェアされるようだ。つまり、Asyncで呼び出した先のコールグラフで何かをAsyncLocal にセットしたとしても、呼び出し元のメソッドに戻るとそれは消えている。

一方、ThreadLocal は名前の通り、同じスレッド内だと値がシェアされる。だから、呼び出し先のメソッドで値を変更したら、呼び出し元が参照した、ThreadLocal の値は呼び出し先で変更されたものになる。

Level1:Start: 1 AsyncLocal: Value 1 ThreadLocal: Value 1 Thread: 1
Level2:Start: 1 AsyncLocal: Value 1.1 ThreadLocal: Value 1.1 Thread: 1
Level2:End: 1 AsyncLocal: Value 1.1.1 ThreadLocal: Value 1.1.1 Thread: 1
Level1:End: 1 AsyncLocal: Value 1.1 ThreadLocal: Value 1.1.1 Thread: 1
Level1:Start: 2 AsyncLocal: Value 2 ThreadLocal: Value 2 Thread: 1
Level2:Start: 2 AsyncLocal: Value 2.2 ThreadLocal: Value 2.2 Thread: 1
Level2:End: 2 AsyncLocal: Value 2.2.2 ThreadLocal: Value 2.2.2 Thread: 1
Level1:End: 2 AsyncLocal: Value 2.2 ThreadLocal: Value 2.2.2 Thread: 1
Level0: AsyncLocal: Value 2 Thread: 1
Level0: ThreadLocal: Value 2.2.2 Thread: 1

実行結果 (複数の Thread)

コードの中にある await Task.Delay(100); をコメントアウトする。こうすることによって、スレッドがつかまれるので、複数のスレッドが利用される。スレッドが異なるので、ThreadLocal の場合は、値が共有されなくなっている。一方 AsyncLocal の方は値は全く同じだ。async/await 型の非同期処理の場合 Task にどのようにスレッドが割り当てられるかはわからないので、一般的には AsyncLocal を使うのがよさそう。もし、更新先での値を変更したい場合は、オブジェクトを共有したらいけるだろう。

Level1:Start: 1 AsyncLocal: Value 1 ThreadLocal: Value 1 Thread: 1
Level1:Start: 2 AsyncLocal: Value 2 ThreadLocal: Value 2 Thread: 1
Level2:Start: 1 AsyncLocal: Value 1.1 ThreadLocal:  Thread: 5
Level2:Start: 2 AsyncLocal: Value 2.2 ThreadLocal:  Thread: 4
Level2:End: 2 AsyncLocal: Value 2.2.2 ThreadLocal: .2 Thread: 4
Level2:End: 1 AsyncLocal: Value 1.1.1 ThreadLocal: .1 Thread: 5
Level1:End: 1 AsyncLocal: Value 1.1 ThreadLocal: .1 Thread: 5
Level1:End: 2 AsyncLocal: Value 2.2 ThreadLocal: .2 Thread: 4
Level0: AsyncLocal: Value 2 Thread: 4
Level0: ThreadLocal: .2 Thread: 4

AsyncLocal をコールグラフ間で共有する

単純にオブジェクトの型にすると、共有してくれる。先ほどのプログラムに、オブジェクト参照で AsyncLocal をつかってみた。 予想通り。

   class Context
    {
        public string Value { get; set; }
    }
    class Program
    {

        private static AsyncLocal<string> asyncLocal = new AsyncLocal<string>();
        private static AsyncLocal<Context> asyncLocalContext = new AsyncLocal<Context>();
        private static ThreadLocal<string> threadLocal = new ThreadLocal<string>();
        static void Main(string[] args)
        {
            var program = new Program();
            program.ExecuteAsync().GetAwaiter().GetResult();
            Console.ReadLine();
        }

        private async Task ExecuteAsync()
        {
            asyncLocal.Value = "Value 1";
            threadLocal.Value = "Value 1";
            asyncLocalContext.Value = new Context();
            asyncLocalContext.Value.Value = "Context Value 1";
            var t1 = Level1("1");
            asyncLocal.Value = "Value 2";
            threadLocal.Value = "Value 2";
            asyncLocalContext.Value = new Context();
            asyncLocalContext.Value.Value = "Context Value 2";
            var t2 = Level1("2");

            await t1;
            await t2;
            Console.WriteLine($"Level0: AsyncLocal: {asyncLocal.Value} AsyncLocalContext: {asyncLocalContext.Value.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"Level0: ThreadLocal: {threadLocal.Value} Thread: {Thread.CurrentThread.ManagedThreadId}");
        }

        private async Task Level1(string value)
        {
            Console.WriteLine($"Level1:Start: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} AsyncLocalContext: {asyncLocalContext.Value.Value}  Thread: {Thread.CurrentThread.ManagedThreadId}");
            asyncLocal.Value = $"{asyncLocal.Value}.{value}";
            asyncLocalContext.Value.Value = $"{asyncLocal.Value}.{value}";
            threadLocal.Value = $"{threadLocal.Value}.{value}";
            await Task.Delay(100);  // If you add wait, then, the it switch the Thread.
            await Level2(value);
            Console.WriteLine($"Level1:End: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} AsyncLocalContext: {asyncLocalContext.Value.Value}  Thread: {Thread.CurrentThread.ManagedThreadId}");

        }

        private async Task Level2(string value)
        {
            Console.WriteLine($"Level2:Start: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} AsyncLocalContext: {asyncLocalContext.Value.Value}  Thread: {Thread.CurrentThread.ManagedThreadId}");
            asyncLocal.Value = $"{asyncLocal.Value}.{value}";
            threadLocal.Value = $"{threadLocal.Value}.{value}";
            asyncLocalContext.Value.Value = $"{asyncLocal.Value}.{value}";
            Console.WriteLine($"Level2:End: {value} AsyncLocal: {asyncLocal.Value} ThreadLocal: {threadLocal.Value} AsyncLocalContext: {asyncLocalContext.Value.Value}  Thread: {Thread.CurrentThread.ManagedThreadId}");

        }
    }

実行結果

Level1:Start: 1 AsyncLocal: Value 1 ThreadLocal: Value 1 AsyncLocalContext: Context Value 1  Thread: 1
Level1:Start: 2 AsyncLocal: Value 2 ThreadLocal: Value 2 AsyncLocalContext: Context Value 2  Thread: 1
Level2:Start: 1 AsyncLocal: Value 1.1 ThreadLocal:  AsyncLocalContext: Value 1.1.1  Thread: 5
Level2:Start: 2 AsyncLocal: Value 2.2 ThreadLocal:  AsyncLocalContext: Value 2.2.2  Thread: 4
Level2:End: 1 AsyncLocal: Value 1.1.1 ThreadLocal: .1 AsyncLocalContext: Value 1.1.1.1  Thread: 5
Level2:End: 2 AsyncLocal: Value 2.2.2 ThreadLocal: .2 AsyncLocalContext: Value 2.2.2.2  Thread: 4
Level1:End: 2 AsyncLocal: Value 2.2 ThreadLocal: .2 AsyncLocalContext: Value 2.2.2.2  Thread: 4
Level1:End: 1 AsyncLocal: Value 1.1 ThreadLocal: .1 AsyncLocalContext: Value 1.1.1.1  Thread: 5
Level0: AsyncLocal: Value 2 AsyncLocalContext: Value 2.2.2.2 Thread: 5
Level0: ThreadLocal: .1 Thread: 5

まとめ

大体これで挙動が理解できたかと思う。

Resource

words

Ambient adj
(especially of environmental conditions) existing in the surrounding area:
Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.

5
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4