38

asyncC#の新しいasync / awaitキーワードを使用すると、コールバックデリゲートが操作が開始されたスレッドとは異なるスレッドで実行されるため、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を使用しても同じことが起こっているのではないでしょうか。asyncawait

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

ありがとう、

4

5 に答える 5

31

andを使用することもできますが、単純な並列処理 ( / )を使用する場合、これらはいかなる種類の「複製」もサポートしないため、使用しないことをお勧めします。CallContext.LogicalSetDataCallContext.LogicalGetDataTask.WhenAnyTask.WhenAll

MSDN フォーラムの投稿で詳しく説明されている、より完全な互換性のある「コンテキスト」を求める UserVoice リクエストを開きました。自分たちで作るのは無理そうです。Jon Skeet は、このテーマに関する優れたブログ エントリを持っています。async

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

はい、 s 間でOperationContext.Currentは保持されませんawait

更新: .NET 4.5 はコードでサポートLogical[Get|Set]Dataされていasyncます。詳細はブログで

于 2012-10-22T12:39:54.277 に答える
9

基本的に、私は強調します:それをしないでください。[ThreadStatic]スレッド間をジャンプするコードではうまく動作しません。

しかし、そうする必要はありません。すでにキャリー状態 - 実際には、2 つのTask異なる方法で行うことができます。

  • 必要なものすべてを保持できる明示的な状態オブジェクトがあります
  • 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;
}
于 2012-10-22T11:39:13.597 に答える
7

タスクの継続を同じスレッドで実行するには、同期プロバイダーが必要です。これは高価な言葉です。簡単な診断は、デバッガーで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派生クラスも作成する必要があります。難しいことですが、Console.ReadLine()のような呼び出しを行うことはできなくなりました。これは、Windows呼び出しのメインスレッドが完全にフリーズするためです。コンソールモードアプリはコンソールアプリではなくなり、Winformsアプリのようになります。

これらのランタイム環境には、正当な理由で同期プロバイダーがあることに注意してください。GUIは基本的にスレッドセーフではないため、1つ持っている必要がありますコンソールの問題ではなく、スレッドセーフです。

于 2012-10-22T12:06:45.347 に答える
0

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

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

Sleepy を呼び出す直前に「Start」内のローカル フィールドに Secret を格納し、Sleepy から戻った後にローカル フィールドから Secret を復元することで問題を解決できます。「await Task.Delay(1000);」を呼び出す直前に Sleepy で実行することもできます。それは実際にスレッドの切り替えを引き起こします。

于 2017-02-10T11:41:26.447 に答える