27

この質問で、Stephen Clearyが受け入れた回答は、LogicalCallContextが非同期で正しく機能しないことを示しています。彼はまた、このMSDNスレッドにそれについて投稿しました。

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

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

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

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

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

ETA:この他の質問も参照してください。そこで、スティーブン・クリアリーによる答えは次のように述べています。

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

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

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

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でキー設定された、不変の構造に個別の値を手動で格納します。(このアプローチにはパフォーマンスの問題があります。たとえば、デッドタスクのデータの保持などですが、それ以外は機能しますか?)

4

2 に答える 2

19

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

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

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

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

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フォーラムの投稿に似た例です(簡単にするために、 SynchronizationContextGUIやASP.NETなどの非並列を想定しています)。

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

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").
}

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

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

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

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

于 2013-01-06T01:01:19.483 に答える
9

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

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

于 2013-01-07T09:32:11.340 に答える