web-dev-qa-db-ja.com

LogicalCallContextがasyncで機能しないのはなぜですか?

この中で 質問 スティーブンクリアリーによって受け入れられた答えは、LogicalCallContextが非同期で正しく機能できないと言っています。彼はまたそれについて this MSDNスレッドに投稿しました。

LogicalCallContextは、CallContext.LogicalGet/SetDataに送信されたデータを格納するハッシュテーブルを保持します。そして、それはこのハッシュテーブルの浅いコピーを行うだけです。したがって、可変オブジェクトをその中に格納すると、異なるタスク/スレッドが互いの変更を確認します。これが、Stephen ClearyのサンプルNDCプログラム(そのMSDNスレッドに投稿されている)が正しく機能しない理由です。

しかし、AFAICSでは、不変のデータのみをハッシュテーブルに格納する場合(おそらく 不変のコレクション を使用して)、それは機能するはずであり、NDCを実装しましょう。

しかし、スティーブン・クリアリーはその受け入れられた答えの中で次のようにも述べています。

CallContextはこれに使用できません。 Microsoftは、特に 推奨 リモート処理以外の目的でCallContextを使用しないようにしています。さらに重要なことに、論理CallContextは、非同期メソッドがどのように早く戻り、後で再開するかを理解していません。

残念ながら、Microsoftの推奨事項へのリンクはダウンしています(ページが見つかりません)。だから私の質問は、なぜこれが推奨されないのですか?この方法でLogicalCallContextを使用できないのはなぜですか?非同期メソッドを理解していないとはどういう意味ですか?呼び出し元のPOVから、それらはタスクを返す単なるメソッドですよね?

ETA: この他の質問 も参照してください。そこで、StephenClearyによる回答は次のように述べています。

callContext.LogicalSetDataとCallContext.LogicalGetDataを使用できますが、単純な並列処理を使用する場合、これらはいかなる種類の「クローン作成」もサポートしないため、使用しないことをお勧めします。

それは私の場合をサポートしているようです。したがって、私はshould NDCを構築できます。これは、log4netだけでなく、実際に必要なものです。

私はいくつかのサンプルコードを書きました、そしてそれはうまくいくようです、しかし単なるテストは必ずしも並行性のバグを捕らえるとは限りません。それで、これらの他の投稿にはこれがうまくいかないかもしれないというヒントがあるので、私はまだ尋ねています:このアプローチは有効ですか?

ETA:以下の答えからスティーブンが提案した再現を実行すると、彼が言う間違った答えは得られません。正しい答えが得られます。彼が「ここのLogicalCallContext値は常に「1」である」と言った場合でも、私は常に正しい値0を取得します。これはおそらく競合状態が原因ですか?とにかく、私はまだ自分のコンピューターで実際の問題を再現していません。これが私が実行している正確なコードです。ここでは「true」のみを出力しますが、Stephenは、少なくとも一部の時間は「false」を出力する必要があると述べています。

private static string key2 = "key2";
private static int Storage2 { 
    get { return (int) CallContext.LogicalGetData(key2); } 
    set { CallContext.LogicalSetData(key2, value);} 
}

private static async Task ParentAsync() {
  //Storage = new Stored(0); // Set LogicalCallContext value to "0".
  Storage2 = 0;

  Task childTaskA = ChildAAsync();
  // LogicalCallContext value here is always "1".
  // -- No, I get 0
  Console.WriteLine(Storage2 == 0);

  Task childTaskB = ChildBAsync();
  // LogicalCallContext value here is always "2".
  // -- No, I get 0
  Console.WriteLine(Storage2 == 0);

  await Task.WhenAll(childTaskA, childTaskB);
  // LogicalCallContext value here may be "0" or "1".
  // -- I always get 0
  Console.WriteLine(Storage2 == 0);
}

private static async Task ChildAAsync() {
  var value = Storage2; // Save LogicalCallContext value (always "0").
  Storage2 = 1; // Set LogicalCallContext value to "1".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "1" or "2".
  Console.WriteLine(Storage2 == 1);

  Storage2 = value; // Restore original LogicalCallContext value (always "0").
}

private static async Task ChildBAsync() {
  var value = Storage2; // Save LogicalCallContext value (always "1").
  Storage2 = 2; // Set LogicalCallContext value to "2".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "0" or "2".
  Console.WriteLine(Storage2 == 2);

  Storage2 = value; // Restore original LogicalCallContext value (always "1").
}

public static void Main(string[] args) {
  try {
    ParentAsync().Wait();
  }
  catch (Exception e) {
    Console.WriteLine(e);
  }

だから私の言い換えた質問は、上記のコードの何が(もしあれば)間違っているのかということです。

さらに、CallContext.LogicalSetDataのコードを見ると、Thread.CurrentThread.GetMutableExecutionContext()が呼び出され、それが変更されています。そしてGetMutableExecutionContextは言う:

if (!this.ExecutionContextBelongsToCurrentScope)
    this.m_ExecutionContext = this.m_ExecutionContext.CreateMutableCopy();
  this.ExecutionContextBelongsToCurrentScope = true;

そして、CreateMutableCopyは最終的に、ユーザー提供のデータを保持するLogicalCallContextのハッシュテーブルの浅いコピーを実行します。

では、なぜこのコードがStephenで機能しないのかを理解しようとすると、ExecutionContextBelongsToCurrentScopeの値が間違っていることがあるからですか?その場合は、現在のタスクIDまたは現在のスレッドIDのいずれかが変更されていることを確認することで、いつ変更されたかを確認し、スレッド+タスクIDでキー設定された個別の値を不変構造に手動で格納できます。 (このアプローチにはパフォーマンスの問題があります。たとえば、デッドタスクのデータの保持などですが、それ以外は機能しますか?)

24
danarmak

Stephenは、これが.Net4.5およびWin8/2012で機能することを確認します。他のプラットフォームではテストされておらず、少なくとも一部のプラットフォームでは機能しないことがわかっています。したがって、答えは、Microsoftがゲームをまとめ、少なくとも最新バージョンの.Netと非同期コンパイラで根本的な問題を修正したということです。

つまり、答えは、古い.Netバージョンでは機能しないということです。 (したがって、log4netプロジェクトはそれを使用して汎用NDCを提供することはできません。)

9
danarmak

更新:この回答は.NET4.5では正しくありません。詳細については、 AsyncLocal に関する私のブログ投稿を参照してください。

これが状況です(あなたの質問でいくつかのポイントを繰り返します):

  • LogicalCallContextasync呼び出しで流れます。 you canこれを使用して暗黙的なデータを設定し、呼び出しスタックのさらに下のasyncメソッドから読み取ります。
  • LogicalCallContextのすべてのコピーは浅いコピーであり、エンドユーザーコードがディープコピーの種類の操作にフックする方法はありません。
  • asyncを使用して「単純な並列処理」を行う場合、さまざまなLogicalCallContextメソッド間にはasyncsharedのコピーが1つだけあります。

LogicalCallContextdoesasyncコードがすべて線形の場合、正常に機能します。

async Task ParentAsync()
{
  ... = 0; // Set LogicalCallContext value to "0".

  await ChildAAsync();
  // LogicalCallContext value here is always "0".

  await ChildBAsync();
  // LogicalCallContext value here is always "0".
}

async Task ChildAAsync()
{
  int value = ...; // Save LogicalCallContext value (always "0").
  ... = 1; // Set LogicalCallContext value to "1".

  await Task.Delay(1000);
  // LogicalCallContext value here is always "1".

  ... = value; // Restore original LogicalCallContext value (always "0").
}

async Task ChildBAsync()
{
  int value = ...; // Save LogicalCallContext value (always "0").
  ... = 2; // Set LogicalCallContext value to "2".

  await Task.Delay(1000);
  // LogicalCallContext value here is always "2".

  ... = value; // Restore original LogicalCallContext value (always "0").
}

しかし、私が「単純な並列処理」と呼んでいるものを使用すると、状況はそれほど良くありません(いくつかのasyncメソッドを開始してから、Task.WaitAll または類似)。これは次のような例です 私のMSDNフォーラムの投稿 (簡単にするために、GUIやASP.NETなどの非並列のSynchronizationContextを想定しています):

編集:コードコメントが正しくありません。この質問と回答のコメントを参照してください

async Task ParentAsync()
{
  ... = 0; // Set LogicalCallContext value to "0".

  Task childTaskA = ChildAAsync();
  // LogicalCallContext value here is always "1".

  Task childTaskB = ChildBAsync();
  // LogicalCallContext value here is always "2".

  await Task.WhenAll(childTaskA, childTaskB);
  // LogicalCallContext value here may be "0" or "1".
}

async Task ChildAAsync()
{
  int value = ...; // Save LogicalCallContext value (always "0").
  ... = 1; // Set LogicalCallContext value to "1".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "1" or "2".

  ... = value; // Restore original LogicalCallContext value (always "0").
}

async Task ChildBAsync()
{
  int value = ...; // Save LogicalCallContext value (always "1").
  ... = 2; // Set LogicalCallContext value to "2".

  await Task.Delay(1000);
  // LogicalCallContext value here may be "0" or "2".

  ... = value; // Restore original LogicalCallContext value (always "1").
}

問題は、LogicalCallContextParentAsyncChildAAsync、およびChildBAsyncの間で共有であり、またはにフックする方法がないことです。ディープコピー操作を強制します。 「線形」の例では、コンテキストも共有されますが、一度にアクティブになったメソッドは1つだけです。

LogicalCallContextに格納するデータが(私の整数の例のように)不変である場合でも、NDCを実装するには、LogicalCallContext値を更新する必要があります。これは、共有を意味します。 -コピーなしの問題はそれを台無しにするでしょう。

私はこれを詳細に調査し、解決策は不可能であると結論付けました。あなたが1つを理解することができれば、私は間違っていると証明されてとてもうれしいです。 :)

P.S. Stephen Toubは、CallContextをリモーティングにのみ使用するという推奨事項(理由もなく与えられた、IIRC)はもはや適用されないと指摘しました。動作させることができれば、LogicalCallContext...を自由に使用できます。 ;)

17
Stephen Cleary