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