web-dev-qa-db-ja.com

非同期/待機でThreadStatic変数を使用する

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.SetDataCallContext.GetDataを使用して実験し、同じ動作をしました。

関連する質問とスレッドを読んだ後:

aSP.Netのようなフレームワークは、スレッド間でHttpContextを明示的に移行しますが、CallContextは移行しないようです。そのため、asyncawaitを使用すると、同じことがおそらく発生します。キーワード?

Async/awaitキーワードの使用を念頭に置いて、コールバックスレッドで(自動的に!)復元できる特定の実行スレッドに関連付けられたデータを保存する最良の方法は何ですか?

おかげで、

35
theburningmonk

あなたcouldCallContext.LogicalSetDataCallContext.LogicalGetDataを使用しますが、単純な並列処理(Task.WhenAny/Task.WhenAll)を使用する場合は、いかなる種類の「クローニング」もサポートしないため、使用しないことをお勧めします。

serVoice request を開いて、より完全なasync互換の「コンテキスト」を作成しました。詳細は MSDNフォーラムの投稿 で説明されています。自分で作ることはできないようです。 Jon Skeetは、この件に関して 良いブログエントリ を持っています。

したがって、Marcが説明したように、引数、ラムダクロージャ、またはローカルインスタンスのメンバー(this)を使用することをお勧めします。

はい、OperationContext.Currentnotawaits全体で保持されます。

更新:。NET 4.5は、asyncコードでLogical[Get|Set]Dataをサポートします。詳細 私のブログ

28
Stephen Cleary

基本的に、私は強調します:そうしないでください。 [ThreadStatic]は、スレッド間をジャンプするコードでうまく機能することは決してありません。

しかし、そうする必要はありません。 Taskはすでに状態を保持しています-実際、それは2つの異なる方法で実行できます。

  • 必要なすべてを保持できる明示的な状態オブジェクトがあります
  • lambdas/anon-methodsは状態に対してクロージャーを形成できます

さらに、コンパイラはここで必要なすべてを実行します。

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;
}
10
Marc Gravell

タスクの継続を同じスレッドで実行するには、同期プロバイダーが必要です。これは高価な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つ持つ必要があります。コンソールの問題ではなく、スレッドセーフです。

6
Hans Passant

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]
1
Joel Oughton

これを見てください スレッド

ThreadStaticAttributeでマークされたフィールドでは、静的コンストラクターで初期化が1回だけ行われます。コードでは、ID 11の新しいスレッドが作成されると、新しいシークレットフィールドが作成されますが、空/ nullです。待機呼び出しの後に「開始」タスクに戻ると、タスクはスレッド11で終了するため(印刷出力に示されているように)、文字列は空です。

Sleepyを呼び出す直前に「Start」内のローカルフィールドにシークレットを保存し、Sleepyから戻った後、ローカルフィールドからシークレットを復元することで問題を解決できます。 「awaitTask.Delay(1000);」を呼び出す直前に、Sleepyでそれを行うこともできます。実際にスレッドの切り替えが発生します。

0
Peterf