9

とCorrelationManager.LogicalOperationStack互換性があります。が背景情報については、この質問を参照してくださいParallel.For、タスク、スレッドなど

How do Tasks in the Task Parallel Library affect ActivityID?

をその質問タスクTrace.CorrelationManager.ActivityIdにどのように影響するかを尋ねます。 @Greg Samsonは、ActivityIdがTasksのコンテキストで信頼できることを示すテストプログラムで自分の質問に答えました。テストプログラムは、タスクデリゲートの先頭にActivityIdを設定し、スリープして作業をシミュレートし、最後にActivityIdをチェックして、同じ値(別のスレッドによって変更されていない)であることを確認します。プログラムは正常に実行されます。

スレッディング、タスク、およびパラレル操作(最終的にはより良いロギングのコンテキストを提供する)のための他の "コンテキスト"オプションを調べているうちに、Trace.CorrelationManager.LogicalOperationStackという奇妙な問題が発生しました。私は下の彼の質問に私の "答え"をコピーしました。

私は、Trace.CorrelationManager.LogicalOperationStackが壊れているように見えます(Parallel.Forのコンテキストで使用される場合は、Parallel.For自体が論理演算)。

ここに私の質問は以下のとおりです。

  1. がParallel.Forで使用可能Trace.CorrelationManager.LogicalOperationStackべきでしょうか?そうであれば、Parallel.Forで論理演算が既に有効になっている場合に変更を加える必要がありますか?

  2. Parallel.ForでLogicalOperationStackを使用する「正しい」方法はありますか?私はこのサンプルプログラムを別の方法で "動作する"ようにコーディングできますか? 「作品」とは、LogicalOperationStackが常に予想されるエントリ数を持ち、エントリ自体が予想されるエントリであることを意味します。

私はスレッドとThreadPoolのスレッドを使用して、いくつかの追加のテストを行っているが、私は戻って、私は同様の問題に遭遇したかどうかを確認するためにこれらのテストを再試行する必要があります。

Task/ParallelスレッドとThreadPoolスレッドは、親スレッドからTrace.CorrelationManager.ActivityIdとTrace.CorrelationManager.LogicalOperationStack値を「継承する」と表示されます。これは、これらの値がCallContextのLogicalSetDataメソッド(SetDataではなく)を使用してCorrelationManagerによって格納されるために必要です。

繰り返しますが、私は以下の投稿「答え」のために元のコンテキストを取得するために戻って、この質問を参照してください。

How do Tasks in the Task Parallel Library affect ActivityID?

も(これまでに回答されていません)この同様の質問を参照してくださいMicrosoftのパラレル拡張機能のフォーラムで:

http://social.msdn.microsoft.com/Forums/en-US/parallelextensions/thread/7c5c3051-133b-4814-9db0-fc0039b4f9d9

01 [PASTEをBEGIN]

CorrelationManagerの動作とスレッド/タスク/などを扱うので、あなたの質問には本当に答えることはできませんが、回答として私の投稿をこれを許してください。私はCorrelationManagerのLogicalOperationStack(およびStartLogicalOperation/StopLogicalOperationメソッド)を使用して、マルチスレッドシナリオで追加のコンテキストを提供する方法を検討してきました。

Parallel.Forを使用して作業を並行して実行する機能を追加するために、私はあなたの例をとり、わずかに変更しました。また、StartLogicalOperation/StopLogicalOperationを使用して(内部で)DoLongRunningWorkを括弧で括っています。概念的には、DoLongRunningWorkは、それが実行されるたびに、このような何かを行います。

DoLongRunningWork 
    StartLogicalOperation 
    Thread.Sleep(3000) 
    StopLogicalOperation 

を私は(あるとして多かれ少なかれ)あなたのコードにこれらの論理演算を追加した場合、論理operatinsのすべてが(同期に残ることを私が発見しました常にスタック上で期待されるオペレーションの数とスタック上のオペレーションの値は常に期待通りです)。

私自身のテストでは、これは必ずしもそうではないことがわかりました。論理演算スタックが壊れていました。私が思いつくことができる最も良い説明は、 "子"スレッドが終了したときにCallContext情報を "親"スレッドコンテキストに戻す "合併"が、 "古い"子スレッドコンテキスト情報(論理演算)を "別の兄弟の子スレッドによって継承されます。

この問題は、Parallel.Forが明らかに "ワーカースレッド"の1つとしてメインスレッド(少なくともサンプルコードでは書かれているように)を使用しているという事実に関連している可能性がありますパラレルドメイン)。 DoLongRunningWorkが実行されるたびに、新しい論理操作が(開始時に)開始され、終了時に停止されます(つまり、LogicalOperationStackにプッシュされ、それからポップされます)。メインスレッドにすでに有効な論理演算があり、DoLongRunningWorkがMAIN THREADで実行すると、新しい論理演算が開始され、メインスレッドのLogicalOperationStackはTWO演算を持つようになります。 DoLongRunningWorkの後続の実行(DoLongRunningWorkのこの "反復"がメインスレッド上で実行されている限り)は、(明らかに)メインスレッドのLogicalOperationStackを継承します(LogicalOperationStackには期待される操作ではなく2つの操作があります)。

私の例でLogicalOperationStackの動作が私の変更されたバージョンのものと異なるのはなぜか分かりませんでした。最後に私のコードでは、プログラム全体を論理演算で囲んだのに対し、テストプログラムの修正版ではそうではありませんでした。私のテストプログラムでは、「仕事」が実行されるたびに(DoLongRunningWorkに類似している)、すでに論理的な操作が有効であったということです。あなたのテストプログラムの私の修正版では、私は論理的な操作でプログラム全体を括弧で囲まなかった。

私は、Parallel.Forを使用している場合、プログラム全体を論理演算で囲むようにテストプログラムを変更しましたが、まったく同じ問題が発生しました。上記の概念モデルを使用して

が、これは正常に実行されます:

Parallel.For 
    DoLongRunningWork 
    StartLogicalOperation 
    Sleep(3000) 
    StopLogicalOperation 

これは最終的に明らかに同期LogicalOperationStackの外に起因して主張しますが:ここ

StartLogicalOperation 
Parallel.For 
    DoLongRunningWork 
    StartLogicalOperation 
    Sleep(3000) 
    StopLogicalOperation 
StopLogicalOperation 

は私のサンプルプログラムです。 ActivityIdとLogicalOperationStackを操作するDoLongRunningWorkメソッドを持っている点で、あなたと似ています。私はDoLongRunningWorkの2つの味があります。 1つのフレーバは、Parallel.Forを使用するタスクを使用します。並列処理全体が論理演算で囲まれるように各フレーバを実行することもできます。したがって、並列操作を実行するための合計4つの方法があります。それぞれを試すには、目的の "使用..."メソッドのコメントを外し、再コンパイルして実行します。UseTasks,UseTasks(true)、およびUseParallelForはすべて完了まで実行する必要があります。 UseParallelFor(true)は、LogicalOperationStackに予想されるエントリ数がないため、ある時点でアサートします。

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Diagnostics; 
using System.Threading; 
using System.Threading.Tasks; 

namespace CorrelationManagerParallelTest 
{ 
    class Program 
    {  
    static void Main(string[] args)  
    { 
     //UseParallelFor(true) will assert because LogicalOperationStack will not have expected 
     //number of entries, all others will run to completion. 

     UseTasks(); //Equivalent to original test program with only the parallelized 
         //operation bracketed in logical operation. 
     ////UseTasks(true); //Bracket entire UseTasks method in logical operation 
     ////UseParallelFor(); //Equivalent to original test program, but use Parallel.For 
          //rather than Tasks. Bracket only the parallelized 
          //operation in logical operation. 
     ////UseParallelFor(true); //Bracket entire UseParallelFor method in logical operation 
    }  

    private static List<int> threadIds = new List<int>();  
    private static object locker = new object();  

    private static int mainThreadId = Thread.CurrentThread.ManagedThreadId; 

    private static int mainThreadUsedInDelegate = 0; 

    // baseCount is the expected number of entries in the LogicalOperationStack 
    // at the time that DoLongRunningWork starts. If the entire operation is bracketed 
    // externally by Start/StopLogicalOperation, then baseCount will be 1. Otherwise, 
    // it will be 0. 
    private static void DoLongRunningWork(int baseCount)  
    { 
     lock (locker) 
     { 
     //Keep a record of the managed thread used.    
     if (!threadIds.Contains(Thread.CurrentThread.ManagedThreadId)) 
      threadIds.Add(Thread.CurrentThread.ManagedThreadId); 

     if (Thread.CurrentThread.ManagedThreadId == mainThreadId) 
     { 
      mainThreadUsedInDelegate++; 
     } 
     }   

     Guid lo1 = Guid.NewGuid(); 
     Trace.CorrelationManager.StartLogicalOperation(lo1); 

     Guid g1 = Guid.NewGuid();   
     Trace.CorrelationManager.ActivityId = g1; 

     Thread.Sleep(3000);   

     Guid g2 = Trace.CorrelationManager.ActivityId; 
     Debug.Assert(g1.Equals(g2)); 

     //This assert, LogicalOperation.Count, will eventually fail if there is a logical operation 
     //in effect when the Parallel.For operation was started. 
     Debug.Assert(Trace.CorrelationManager.LogicalOperationStack.Count == baseCount + 1, string.Format("MainThread = {0}, Thread = {1}, Count = {2}, ExpectedCount = {3}", mainThreadId, Thread.CurrentThread.ManagedThreadId, Trace.CorrelationManager.LogicalOperationStack.Count, baseCount + 1)); 
     Debug.Assert(Trace.CorrelationManager.LogicalOperationStack.Peek().Equals(lo1), string.Format("MainThread = {0}, Thread = {1}, Count = {2}, ExpectedCount = {3}", mainThreadId, Thread.CurrentThread.ManagedThreadId, Trace.CorrelationManager.LogicalOperationStack.Peek(), lo1)); 

     Trace.CorrelationManager.StopLogicalOperation(); 
    } 

    private static void UseTasks(bool encloseInLogicalOperation = false) 
    { 
     int totalThreads = 100; 
     TaskCreationOptions taskCreationOpt = TaskCreationOptions.None; 
     Task task = null; 
     Stopwatch stopwatch = new Stopwatch(); 
     stopwatch.Start(); 

     if (encloseInLogicalOperation) 
     { 
     Trace.CorrelationManager.StartLogicalOperation(); 
     } 

     Task[] allTasks = new Task[totalThreads]; 
     for (int i = 0; i < totalThreads; i++) 
     { 
     task = Task.Factory.StartNew(() => 
     { 
      DoLongRunningWork(encloseInLogicalOperation ? 1 : 0); 
     }, taskCreationOpt); 
     allTasks[i] = task; 
     } 
     Task.WaitAll(allTasks); 

     if (encloseInLogicalOperation) 
     { 
     Trace.CorrelationManager.StopLogicalOperation(); 
     } 

     stopwatch.Stop(); 
     Console.WriteLine(String.Format("Completed {0} tasks in {1} milliseconds", totalThreads, stopwatch.ElapsedMilliseconds)); 
     Console.WriteLine(String.Format("Used {0} threads", threadIds.Count)); 
     Console.WriteLine(String.Format("Main thread used in delegate {0} times", mainThreadUsedInDelegate)); 

     Console.ReadKey(); 
    } 

    private static void UseParallelFor(bool encloseInLogicalOperation = false) 
    { 
     int totalThreads = 100; 
     Stopwatch stopwatch = new Stopwatch(); 
     stopwatch.Start(); 

     if (encloseInLogicalOperation) 
     { 
     Trace.CorrelationManager.StartLogicalOperation(); 
     } 

     Parallel.For(0, totalThreads, i => 
     { 
     DoLongRunningWork(encloseInLogicalOperation ? 1 : 0); 
     }); 

     if (encloseInLogicalOperation) 
     { 
     Trace.CorrelationManager.StopLogicalOperation(); 
     } 

     stopwatch.Stop(); 
     Console.WriteLine(String.Format("Completed {0} tasks in {1} milliseconds", totalThreads, stopwatch.ElapsedMilliseconds)); 
     Console.WriteLine(String.Format("Used {0} threads", threadIds.Count)); 
     Console.WriteLine(String.Format("Main thread used in delegate {0} times", mainThreadUsedInDelegate)); 

     Console.ReadKey(); 
    } 

    } 
} 

この全体のLogicalOperationStackはParallel.Forで使用することができる場合の問題(および/または他のスレッド/タスクの構築)、またはどのようにそれがメリットに、おそらく、独自の質問を使用することができます。たぶん私は質問を投稿します。その間、私はあなたにこれについての考えがあるかどうか疑問に思います(または、ActivityIdが安全であると思われるのでLogicalOperationStackの使用を検討していたのだろうか)。

[ENDのPASTE]

誰もがこの問題上の任意の考えを持っていますか?

+0

LogicalOperationStackとParallel.Forに関するご意見やご提案はありますか? – wageoghe

+0

犯人を探すのを救済してくれた徹底的な調査に感謝します:) –

答えて

5

[アップデートを開始]私はまたMicrosoft's Parallel Extensions for .Net support forumにこの質問をし、最終的にanswer from Stephen Toubを受けました。 LogicalOperationStackが破損する原因となっているのはbug in the LogicalCallContextです。また、Parallel.ForがTaskをどのように処理するかについての簡単な概要とその理由がParallel.Forをバグの影響を受けやすいものにする素晴らしい記述(スティーブンの回答には、私が彼の答えを書いてくれた)があります。

私の答えでは、Parallel.Forはメインスレッドを「ワーカー」スレッドの1つとして使用するため、LogicalOperationStackはParallel.Forと互換性がないと推測しています。スティーブンの説明によると、私の推測は間違っていた。 Parallel.Forは "ワーカー"スレッドの1つとしてメインスレッドを使用しますが、単に "そのまま"使用されるわけではありません。最初のタスクはメインスレッドで実行されますが、新しいスレッドで実行されているかのように実行されます。詳細はスティーブンの説明を読んでください。

[終了更新]

私が言うことができるものから、次のように、答えは次のとおりです。

のActivityIDとLogicalOperationStackの両方がCallContext.LogicalSetDataを経由して保存されます。これは、これらの値が「子」スレッドに「フロー」されることを意味します。たとえば、エントリポイントでActivityIdをマルチスレッドサーバー(サービスコールなど)に設定し、そのエントリポイントから最終的に開始されるすべてのスレッドを同じ「アクティビティ」に含めることができます。同様に、LogicalOperationStackによる論理演算も子スレッドに流れます。 Trace.CorrelationManager.ActivityIdに関して

のActivityIDは、私はそれをテストしているすべてのスレッドモデルと互換性があるようだ:。、直接のスレッドを使用してのThreadPoolを使用して、タスクを使用して、パラレルを使用して*。いずれの場合も、ActivityIdは期待値です。 Trace.CorrelationManager.LogicalOperationStackに関して

LogicalOperationStackではなく、並行して、ほとんどのスレッドモデルと互換性があるようです*。。スレッド、ThreadPool、およびTasksを使用して、LogicalOperationStack(私の質問で提供されているサンプルコードで操作)は、その整合性を維持します。 LogicalOperationStackの内容はいつも期待どおりです。

LogicalOperationStackはParallel.Forと互換性がありません。論理演算が「有効」である場合、つまり、CorrelationManagerを呼び出した場合です。Parallel。*操作を開始する前にStartLogicalOperationを実行し、Paralle。*(つまり、デリゲート内)のコンテキストで新しい論理演算を開始すると、LogicalOperationStackが破損します。 (Parallel。*は追加のスレッドを作成しないかもしれません。つまり、LogicalOperationStackは安全です)。

この問題は、Parallel。*が「ワーカー」スレッドの1つとしてメインスレッド(または、おそらくより正確には並列操作を開始するスレッド)を使用していることに起因します。つまり、「メイン」スレッドと同じ「ワーカー」スレッドで「論理演算」が開始および停止されると、「メイン」スレッドのLogicalOperationStackが変更されています。呼び出しコード(すなわちデリゲート)がスタックを正しく維持していても(各StartLogicalOperationが対応するStopLogicalOperationで「停止」されていることを保証する)、「メイン」スレッドスタックは変更されます。最終的には、「メイン」スレッドのLogicalOperationStackは基本的に2つの異なる「論理」スレッド、すなわち「メイン」スレッドと「ワーカー」スレッドによって変更されているようです糸。

私は、少なくともこれがうまくいかない理由について詳しくは分かりません。私の推測では、デリゲートがスレッド(メインスレッドと同じではない)上で実行されるたびに、スレッドはメインスレッドのLogicalOperationStackの現在の状態を「継承」するということです。デリゲートが現在メインスレッド上で(ワーカースレッドとして再利用されて)実行され、論理操作を開始した場合、他の並列デリゲートの1人(または複数の)が、現在保持しているメインスレッドのLogicalOperationStackを「継承」します1つ(またはそれ以上)の新しい論理演算が有効です!

FWIW、LogicalOperationStackを模倣する以下の "論理スタック"をParallelで動作させるように実装しました(主にテスト用ですが、現時点では実際に使用していません)試してみたり、使用したりしてください。これは、ここでブレントVanderMeideにより記載の範囲オブジェクトに触発された

LogicalOperation.OperationStack.Push()/Pop(). 


//OperationStack.cs 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 

using System.Runtime.Remoting.Messaging; 

namespace LogicalOperation 
{ 
    public static class OperationStack 
    { 
    private const string OperationStackSlot = "OperationStackSlot"; 

    public static IDisposable Push(string operation) 
    { 
     OperationStackItem parent = CallContext.LogicalGetData(OperationStackSlot) as OperationStackItem; 
     OperationStackItem op = new OperationStackItem(parent, operation); 
     CallContext.LogicalSetData(OperationStackSlot, op); 
     return op; 
    } 

    public static object Pop() 
    { 
     OperationStackItem current = CallContext.LogicalGetData(OperationStackSlot) as OperationStackItem; 

     if (current != null) 
     { 
     CallContext.LogicalSetData(OperationStackSlot, current.Parent); 
     return current.Operation; 
     } 
     else 
     { 
     CallContext.FreeNamedDataSlot(OperationStackSlot); 
     } 
     return null; 
    } 

    public static object Peek() 
    { 
     OperationStackItem top = Top(); 
     return top != null ? top.Operation : null; 
    } 

    internal static OperationStackItem Top() 
    { 
     OperationStackItem top = CallContext.LogicalGetData(OperationStackSlot) as OperationStackItem; 
     return top; 
    } 

    public static IEnumerable<object> Operations() 
    { 
     OperationStackItem current = Top(); 
     while (current != null) 
     { 
     yield return current.Operation; 
     current = current.Parent; 
     } 
    } 

    public static int Count 
    { 
     get 
     { 
     OperationStackItem top = Top(); 
     return top == null ? 0 : top.Depth; 
     } 
    } 

    public static IEnumerable<string> OperationStrings() 
    { 
     foreach (object o in Operations()) 
     { 
     yield return o.ToString(); 
     } 
    } 
    } 
} 


//OperationStackItem.cs 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 

namespace LogicalOperation 
{ 
    public class OperationStackItem : IDisposable 
    { 
    private OperationStackItem parent = null; 
    private object operation; 
    private int depth; 
    private bool disposed = false; 

    internal OperationStackItem(OperationStackItem parentOperation, object operation) 
    { 
     parent = parentOperation; 
     this.operation = operation; 
     depth = parent == null ? 1 : parent.Depth + 1; 
    } 

    internal object Operation { get { return operation; } } 
    internal int Depth { get { return depth; } } 

    internal OperationStackItem Parent { get { return parent; } } 

    public override string ToString() 
    { 
     return operation != null ? operation.ToString() : ""; 
    } 

    #region IDisposable Members 

    public void Dispose() 
    { 
     if (disposed) return; 

     OperationStack.Pop(); 

     disposed = true; 
    } 

    #endregion 
    } 
} 

への呼び出しと私の元の質問からのサンプルコードで

Trace.CorrelationManager.StartLogicalOperation/StopLogicalOperation 

への呼び出しを置き換える、テストするには:http://www.dnrtv.com/default.aspx?showNum=114

このクラスは次のように使用できます:

public void MyFunc() 
{ 
    using (LogicalOperation.OperationStack.Push("MyFunc")) 
    { 
    MyOtherFunc(); 
    } 
} 

public void MyOtherFunc() 
{ 
    using (LogicalOperation.OperationStack.Push("MyOtherFunc")) 
    { 
    MyFinalFunc(); 
    } 
} 

public void MyFinalFunc() 
{ 
    using (LogicalOperation.OperationStack.Push("MyFinalFunc")) 
    { 
    Console.WriteLine("Hello"); 
    } 
} 
2

TPLを大量に使用するアプリケーションで簡単に動作する論理スタックを作成する方法を示しています。 LogicalOperationStackは、既存のコードを変更せずに必要なものすべてを実行したため、使用することにしました。だから、(あなたILSpyありがとう)私はこのバグの回避策を見つけることを試みたと私はそれがTPLのために働いてしまったと思います

https://connect.microsoft.com/VisualStudio/feedback/details/609929/logicalcallcontext-clone-bug-when-correlationmanager-slot-is-present

public static class FixLogicalOperationStackBug 
{ 
    private static bool _fixed = false; 

    public static void Fix() 
    { 
     if (!_fixed) 
     { 
      _fixed = true; 

      Type taskType = typeof(Task); 
      var s_ecCallbackField = taskType.GetFields(BindingFlags.Static | BindingFlags.NonPublic).First(f => f.Name == "s_ecCallback"); 
      ContextCallback s_ecCallback = (ContextCallback)s_ecCallbackField.GetValue(null); 

      ContextCallback injectedCallback = new ContextCallback(obj => 
      { 
       // Next line will set the private field m_IsCorrelationMgr of LogicalCallContext which isn't cloned 
       CallContext.LogicalSetData("System.Diagnostics.Trace.CorrelationManagerSlot", Trace.CorrelationManager.LogicalOperationStack); 
       s_ecCallback(obj); 
      }); 

      s_ecCallbackField.SetValue(null, injectedCallback); 
     } 
    } 
} 
しかし、私はLogicalCallContextのバグについて読みます
+0

このメソッドをどこで呼び出してバグを「修正」するかについての詳細を追加できますか? – jpierson

+0

この修正プログラムを試してみると、呼び出す前にs_ecCallbackの周りにnullチェックを追加するとNullReferenceExceptionが発生し、Fix()を呼び出した後にテストコードがフリーズすることがあります。 – jpierson

+0

@jpierson:コールバックでヌルチェックを実行すると、タスクが実際に実行されないようにすることができます。ヌルチェックが必要な場合は、s_ecCallbackの割り当て直後になります。しかし、私は、Fixへのあなたの呼び出しが早すぎると推測します。フレームワークが割り当てを変更する前であっても。 –

関連する問題