C#の新しいasync/awaitキーワードを使用すると、async
操作が開始された1つのスレッドとは別のスレッドでコールバックデリゲートが実行されるため、ThreadStaticデータを使用する方法とタイミングに影響があります。たとえば、次のシンプルなコンソールアプリ:
[ThreadStatic]
private static string Secret;
static void Main(string[] args)
{
Start().Wait();
Console.ReadKey();
}
private static async Task Start()
{
Secret = "moo moo";
Console.WriteLine("Started on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [{0}]", Secret);
await Sleepy();
Console.WriteLine("Finished on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [{0}]", Secret);
}
private static async Task Sleepy()
{
Console.WriteLine("Was on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("Now on thread [{0}]", Thread.CurrentThread.ManagedThreadId);
}
次の行に沿って何かを出力します:
Started on thread [9]
Secret is [moo moo]
Was on thread [9]
Now on thread [11]
Finished on thread [11]
Secret is []
また、CallContext.SetData
とCallContext.GetData
を使用して実験し、同じ動作をしました。
関連する質問とスレッドを読んだ後:
aSP.Netのようなフレームワークは、スレッド間でHttpContextを明示的に移行しますが、CallContext
は移行しないようです。そのため、async
とawait
を使用すると、同じことがおそらく発生します。キーワード?
Async/awaitキーワードの使用を念頭に置いて、コールバックスレッドで(自動的に!)復元できる特定の実行スレッドに関連付けられたデータを保存する最良の方法は何ですか?
おかげで、
あなたcouldはCallContext.LogicalSetData
とCallContext.LogicalGetData
を使用しますが、単純な並列処理(Task.WhenAny
/Task.WhenAll
)を使用する場合は、いかなる種類の「クローニング」もサポートしないため、使用しないことをお勧めします。
serVoice request を開いて、より完全なasync
互換の「コンテキスト」を作成しました。詳細は MSDNフォーラムの投稿 で説明されています。自分で作ることはできないようです。 Jon Skeetは、この件に関して 良いブログエントリ を持っています。
したがって、Marcが説明したように、引数、ラムダクロージャ、またはローカルインスタンスのメンバー(this
)を使用することをお勧めします。
はい、OperationContext.Current
はnotawait
s全体で保持されます。
更新:。NET 4.5は、async
コードでLogical[Get|Set]Data
をサポートします。詳細 私のブログ 。
基本的に、私は強調します:そうしないでください。 [ThreadStatic]
は、スレッド間をジャンプするコードでうまく機能することは決してありません。
しかし、そうする必要はありません。 Task
はすでに状態を保持しています-実際、それは2つの異なる方法で実行できます。
さらに、コンパイラはここで必要なすべてを実行します。
private static async Task Start()
{
string secret = "moo moo";
Console.WriteLine("Started on thread [{0}]",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [{0}]", secret);
await Sleepy();
Console.WriteLine("Finished on thread [{0}]",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Secret is [{0}]", secret);
}
静的状態はありません。スレッドや複数のタスクに問題はありません。それうまくいく。ここでsecret
は単なる「ローカル」ではないことに注意してください。コンパイラは、イテレータブロックやキャプチャされた変数の場合と同様に、いくつかのブードゥーを実行しました。リフレクターをチェックすると、次のようになります。
[CompilerGenerated]
private struct <Start>d__0 : IAsyncStateMachine
{
// ... lots more here not shown
public string <secret>5__1;
}
タスクの継続を同じスレッドで実行するには、同期プロバイダーが必要です。これは高価なWordです。簡単な診断は、デバッガーでSystem.Threading.SynchronizationContext.Currentの値を調べることです。
その値は、コンソールモードアプリではnullになります。コンソールモードアプリの特定のスレッドでコードを実行できるプロバイダーはありません。 WinformsまたはWPFアプリまたはASP.NETアプリのみがプロバイダーを持ちます。そして、彼らのメインスレッドでのみ。
これらのアプリのメインスレッドは、非常に特別な処理を行います。ディスパッチャループ(メッセージループまたはメッセージポンプ)があります。 生産者/消費者問題 の一般的な解決策を実装します。ディスパッチャループがスレッドに実行する作業を少し渡すことを可能にします。このようなちょっとした作業は、await式の後のタスクの継続になります。そして、そのビットはディスパッチャスレッドで実行されます。
WindowsFormsSynchronizationContextは、Winformsアプリの同期プロバイダーです。 Control.Begin/Invoke()を使用してリクエストをディスパッチします。 WPFの場合、これはDispatcherSynchronizationContextクラスであり、Dispatcher.Begin/Invoke()を使用して要求をディスパッチします。 ASP.NETの場合、これはAspNetSynchronizationContextクラスであり、非表示の内部配管を使用します。それらは、初期化時にそれぞれのプロバイダーのインスタンスを作成し、それをSynchronizationContext.Currentに割り当てます。
コンソールモードアプリにはそのようなプロバイダーはありません。主にメインスレッドが完全に不適切であるため、ディスパッチャループを使用しません。独自のクラスを作成してから、独自のSynchronizationContext派生クラスも作成します。 Windowsの呼び出しでメインスレッドが完全にフリーズするため、Console.ReadLine()のような呼び出しを行うことはできません。コンソールモードアプリはコンソールアプリではなくなり、Winformsアプリに似たものになります。
これらのランタイム環境には、正当な理由で同期プロバイダーがあることに注意してください。 GUIは基本的にスレッドセーフではないので、それらは1つ持つ必要があります。コンソールの問題ではなく、スレッドセーフです。
AsyncLocal <T> は、特定の非同期コードフローにスコープされた変数を維持するためのサポートを提供します。
変数タイプをAsyncLocalに変更します(例:
private static AsyncLocal<string> Secret = new AsyncLocal<string>();
次の望ましい出力が得られます。
Started on thread [5]
Secret is [moo moo]
Was on thread [5]
Now on thread [6]
Finished on thread [6]
Secret is [moo moo]
これを見てください スレッド
ThreadStaticAttributeでマークされたフィールドでは、静的コンストラクターで初期化が1回だけ行われます。コードでは、ID 11の新しいスレッドが作成されると、新しいシークレットフィールドが作成されますが、空/ nullです。待機呼び出しの後に「開始」タスクに戻ると、タスクはスレッド11で終了するため(印刷出力に示されているように)、文字列は空です。
Sleepyを呼び出す直前に「Start」内のローカルフィールドにシークレットを保存し、Sleepyから戻った後、ローカルフィールドからシークレットを復元することで問題を解決できます。 「awaitTask.Delay(1000);」を呼び出す直前に、Sleepyでそれを行うこともできます。実際にスレッドの切り替えが発生します。