今貢献しているプロジェクトで、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.