2010-12-02 7 views
11

Bevor ich die Task Parallel Library verwendet habe, habe ich CorrelationManager.ActivityId oft verwendet, um Trace-/Fehlerberichte mit mehreren Threads zu verfolgen.Wie wirken sich Tasks in der parallelen Taskbibliothek auf ActivityID aus?

ActivityId wird im lokalen Thread-Speicher gespeichert, sodass jeder Thread seine eigene Kopie erhält. Die Idee ist, dass Sie beim Starten eines Threads (Aktivität) eine neue ActivityId zuweisen. Die ActivityId wird mit anderen Trace-Informationen in die Logs geschrieben, wodurch es möglich wird, die Trace-Informationen für eine einzelne 'Aktivität' herauszufiltern. Dies ist bei WCF sehr nützlich, da die ActivityId auf die Servicekomponente übertragen werden kann.

Hier ist ein Beispiel dafür, was ich spreche:

static void Main(string[] args) 
{ 
    ThreadPool.QueueUserWorkItem(new WaitCallback((o) => 
    { 
     DoWork(); 
    })); 
} 

static void DoWork() 
{ 
    try 
    { 
     Trace.CorrelationManager.ActivityId = Guid.NewGuid(); 
     //The functions below contain tracing which logs the ActivityID. 
     CallFunction1(); 
     CallFunction2(); 
     CallFunction3(); 
    } 
    catch (Exception ex) 
    { 
     Trace.Write(Trace.CorrelationManager.ActivityId + " " + ex.ToString()); 
    } 
} 

Jetzt, mit dem TPL, mein Verständnis ist, dass mehrere Aufgaben Aktie Themen. Bedeutet dies, dass ActivityId in der Mitte der Aufgabe neu initialisiert werden kann (durch eine andere Aufgabe)? Gibt es einen neuen Mechanismus für die Verfolgung von Aktivitäten?

+0

Ich habe nichts zu bieten, aber ich bin auch an diesem Problem interessiert. Es scheint, dass dieselbe Frage auch für Informationen gilt, die allgemein mit CallContext.LogicalSetData festgelegt wurden, da dies die Technologie ist, die der Trace.CorrelationManager zum Speichern der ActivityId und des LogicalOperationStack verwendet. – wageoghe

+0

@wagageohe - Ich habe es endlich geschafft, dies heute zu testen, habe meine Ergebnisse gepostet :) –

+0

Ich habe einige weitere Details in meiner Antwort gepostet. Ich habe auch einen Link zu einer anderen Antwort hier auf SO gepostet, eine neue Frage, die ich hier auf SO gestellt habe, sowie eine Frage, die ich auf dem Parallel Extensions Forum von Microsoft gestellt habe (aber noch nicht zum 21.01.2011 beantwortet wurde) . Vielleicht finden Sie die Informationen nützlich, vielleicht auch nicht. – wageoghe

Antwort

6

Ich habe einige Experimente durchgeführt und es stellt sich heraus, dass die Annahme in meiner Frage falsch ist - mehrere Aufgaben, die mit der TPL erstellt wurden, laufen nicht auf demselben Thread zur selben Zeit.

ThreadLocalStorage kann sicher mit TPL in .NET 4.0 verwendet werden, da ein Thread nur von einer Aufgabe gleichzeitig verwendet werden kann.

Die Annahme, dass Aufgaben gleichzeitig Threads wurde auf einem Interview basiert teilen kann ich über C# 5.0 auf DotNetRocks gehört (sorry, ich kann mich nicht erinnern, welche zeigen, es war) - so meine Frage kann (oder auch nicht) werden bald relevant.

Mein Experiment startet eine Reihe von Aufgaben und zeichnet auf, wie viele Aufgaben ausgeführt wurden, wie lange sie dauerte und wie viele Threads verbraucht wurden. Der Code ist unten, wenn jemand es wiederholen möchte.

class Program 
{ 
    static void Main(string[] args) 
    { 
     int totalThreads = 100; 
     TaskCreationOptions taskCreationOpt = TaskCreationOptions.None; 
     Task task = null; 
     Stopwatch stopwatch = new Stopwatch(); 
     stopwatch.Start(); 
     Task[] allTasks = new Task[totalThreads]; 
     for (int i = 0; i < totalThreads; i++) 
     { 
      task = Task.Factory.StartNew(() => 
      { 
       DoLongRunningWork(); 
      }, taskCreationOpt); 

      allTasks[i] = task; 
     } 

     Task.WaitAll(allTasks); 
     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.ReadKey(); 
    } 


    private static List<int> threadIds = new List<int>(); 
    private static object locker = new object(); 
    private static void DoLongRunningWork() 
    { 
     lock (locker) 
     { 
      //Keep a record of the managed thread used. 
      if (!threadIds.Contains(Thread.CurrentThread.ManagedThreadId)) 
       threadIds.Add(Thread.CurrentThread.ManagedThreadId); 
     } 
     Guid g1 = Guid.NewGuid(); 
     Trace.CorrelationManager.ActivityId = g1; 
     Thread.Sleep(3000); 
     Guid g2 = Trace.CorrelationManager.ActivityId; 
     Debug.Assert(g1.Equals(g2)); 
    } 
} 

Der Ausgang (natürlich wird dies auf der Maschine ab) war:

Completed 100 tasks in 23097 milliseconds 
Used 23 threads 

Ändern taskCreationOpt zu TaskCreationOptions.LongRunning unterschiedliche Ergebnisse haben:

Completed 100 tasks in 3458 milliseconds 
Used 100 threads 
+0

Interessante Ergebnisse. Ich habe etwas Interessantes über Trace.CorrelationManager.ActivityId mit einem Testprogramm gefunden, das auf Ihrem Code basiert. Wenn Sie Ihren Code so verwenden (mehr oder weniger) und Trace.CorrelationManager.StartLogicalOperation/StopLogicalOperation verwenden, kann ich "gute" Ergebnisse erhalten. Das heißt, mit Tasks auf die Art und Weise, wie Sie jede Aufgabe (innerhalb des Delegaten) mit StartLogicalOperation/StopLogicalOperation demonstrieren und einklammern, scheint der LogicalOperationStack immer synchron zu sein. Die Verwendung von Parallel.For kann jedoch zu schlechten Ergebnissen führen. Ich werde meinen Test als Antwort veröffentlichen, da er Code – wageoghe

+0

hat Große Antwort; danke fürs Schreiben. Eine pedantische Frage: Das Feld "List threadIds" sollte wahrscheinlich ein ConcurrentBag oder ConcurrentDictionary anstelle einer unsynchronisierten generischen Liste sein (das Wörterbuch, zB wenn Sie die taskId der threadId zuordnen oder einfach als gleichzeitige Hashtabelle verwenden wollten) & ignoriere den Wert. –

3

Bitte verzeiht mein Posting dieses als Antwort, da es sich nicht wirklich um eine Antwort auf Ihre Frage handelt, sondern weil es sich um CorrelationManager-Verhalten und Threads/Tasks/etc handelt. Ich habe mir die LogicalOperationStack (und StartLogicalOperation/StopLogicalOperation) Methoden des CorrelationManager angesehen, um zusätzlichen Kontext in Multithreading-Szenarien bereitzustellen.

Ich nahm Ihr Beispiel und änderte es geringfügig, um die Fähigkeit zu ergänzen, Arbeit parallel mit Parallel.For durchzuführen. Außerdem verwende ich StartLogicalOperation/StopLogicalOperation, um (intern) DoLongRunningWork zu klammern. Konzeptionell tut DoLongRunningWork so etwas wie dies jedes Mal ausgeführt wird:

DoLongRunningWork 
    StartLogicalOperation 
    Thread.Sleep(3000) 
    StopLogicalOperation 

ich gefunden habe, dass, wenn ich diese logischen Operationen an Ihrem Code hinzufügen (mehr oder weniger wie), alle logischen operatins synchron bleiben (immer die erwartete Anzahl von Operationen auf Stack und die Werte der Operationen auf dem Stack sind immer wie erwartet).

In einigen meiner eigenen Tests fand ich, dass dies nicht immer der Fall war. Der Stapel logischer Operationen wurde "beschädigt". Die beste Erklärung, die ich finden könnte, ist, dass das "Zusammenführen" der CallContext-Informationen in den "Eltern" -Threadkontext, wenn der "Kind" -Thread beendet wird, die "alte" Kindthreadkontextinformation (logische Operation) verursacht hat. geerbt "von einem anderen Geschwister-Kind-Thread. Das Problem könnte auch damit zusammenhängen, dass Parallel.For anscheinend den Hauptthread (zumindest im Beispielcode, so wie er geschrieben wurde) als einen der "Worker Threads" (oder wie auch immer sie in der parallele Domäne). Immer wenn DoLongRunningWork ausgeführt wird, wird eine neue logische Operation gestartet (am Anfang) und gestoppt (am Ende) (dh auf den LogicalOperationStack geschoben und wieder entfernt). Wenn der Hauptthread bereits eine logische Operation ausführt und DoLongRunningWork über den Hauptthread ausgeführt wird, wird eine neue logische Operation gestartet, sodass der LogicalOperationStack des Hauptthreads nun ZWEI Operationen aufweist. Jede weitere Ausführung von DoLongRunningWork (solange diese "Iteration" von DoLongRunningWork im Hauptthread ausgeführt wird) wird (scheinbar) den LogicalOperationStack des Hauptthreads erben (der jetzt zwei Operationen hat und nicht nur die eine erwartete Operation).

Es hat lange gedauert, bis ich herausgefunden habe, warum das Verhalten von LogicalOperationStack in meinem Beispiel anders ist als in meiner modifizierten Version Ihres Beispiels. Schließlich sah ich, dass ich in meinem Code das gesamte Programm in eine logische Operation eingeklammert hatte, während ich das in meiner modifizierten Version Ihres Testprogramms nicht getan hatte. Die Implikation ist, dass in meinem Testprogramm jedes Mal, wenn meine "Arbeit" ausgeführt wurde (analog zu DoLongRunningWork), bereits eine logische Operation in Kraft war. In meiner modifizierten Version Ihres Testprogramms hatte ich das gesamte Programm nicht in einer logischen Operation eingeklammert.

Also, wenn ich Ihr Testprogramm geändert, um das gesamte Programm in einer logischen Operation und wenn ich Parallel.For Klammern, lief ich genau das gleiche Problem.

das konzeptionelle Modell oben verwenden, wird dies erfolgreich ausgeführt:

Parallel.For 
    DoLongRunningWork 
    StartLogicalOperation 
    Sleep(3000) 
    StopLogicalOperation 

Während dies schließlich aufgrund eines durchsetzen wird offenbar nicht synchron Logicaloperation:

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

Hier ist mein Beispielprogramm ist. Es ähnelt Ihrer, indem es eine DoLongRunningWork-Methode enthält, die sowohl die ActivityId als auch den LogicalOperationStack manipuliert. Ich habe auch zwei Arten von Treten von DoLongRunningWork. Ein Flavor verwendet Tasks, die Parallel.For verwenden. Jede Geschmacksrichtung kann auch so ausgeführt werden, dass die gesamte parallelisierte Operation in einer logischen Operation eingeschlossen ist oder nicht. Es gibt also insgesamt 4 Möglichkeiten, den Parallelbetrieb auszuführen. Um die einzelnen zu testen, entfernen Sie einfach die gewünschte "Use ..." -Methode, kompilieren Sie sie und führen Sie sie aus. UseTasks, UseTasks(true) und UseParallelFor sollten alle vollständig ausgeführt werden. UseParallelFor(true) wird zu einem bestimmten Zeitpunkt aktiviert, da der LogicalOperationStack nicht die erwartete Anzahl von Einträgen hat.

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(); 
    } 

    } 
} 

Dieses ganze Thema, wenn Logicaloperation mit Parallel.For verwendet werden (und/oder andere Threading/Aufgabe Konstrukte) oder wie kann es wohl verdient seine eigene Frage verwendet werden. Vielleicht werde ich eine Frage stellen. In der Zwischenzeit frage ich mich, ob Sie irgendwelche Gedanken dazu haben (oder ich frage mich, ob Sie LogicalOperationStack in Betracht gezogen haben, da ActivityId sicher zu sein scheint).

[EDIT]

Siehe meine Antwort auf this question für weitere Informationen über die Verwendung von Logicaloperation und/oder CallContext.LogicalSetData mit einigen der verschiedenen Thema/Thread/Aufgabe/Parallel contstructs.

Siehe auch hier meine Frage auf SO über Logicaloperation und parallele Erweiterungen: Is CorrelationManager.LogicalOperationStack compatible with Parallel.For, Tasks, Threads, etc

Schließlich sieht auch meine Frage hier auf Parallel Extensions Forum von Microsoft: http://social.msdn.microsoft.com/Forums/en-US/parallelextensions/thread/7c5c3051-133b-4814-9db0-fc0039b4f9d9

In meinen Tests es wie Trace aussieht. CorrelationManager.LogicalOperationStack kann bei Verwendung von Parallel.For oder Parallel.Invoke beschädigt werden, wenn Sie eine logische Operation im Hauptthread starten und dann logische Operationen im Delegate starten/stoppen. In meinen Tests (siehe eine der beiden obigen Links) sollte der LogicalOperationStack immer genau 2 Einträge haben, wenn DoLongRunningWork ausgeführt wird (wenn ich eine logische Operation im Haupt-Thread starte, bevor ich DoLongRunningWork mit verschiedenen Techniken starte). Also, mit "beschädigt" meine ich, dass der LogicalOperationStack schließlich mehr als 2 Einträge haben wird.

Von was ich sagen kann, ist dies wahrscheinlich, weil Parallel.For und Parallel.Invoke den Hauptthread als einen der "Worker" -Threads verwenden, um die DoLongRunningWork-Aktion auszuführen.

Die Verwendung eines Stacks, der in CallContext.LogicalSetData gespeichert ist, um das Verhalten von LogicalOperationStack nachzuahmen (ähnlich Log4nets LogicalThreadContext.Stacks, das über CallContext.SetData gespeichert wird), liefert noch schlechtere Ergebnisse. Wenn ich einen solchen Stapel verwende, um den Kontext zu erhalten, wird er in fast allen Szenarien, in denen ich eine "logische Operation" im Haupt-Thread und eine logische Operation in jeder Iteration habe, beschädigt (dh hat nicht die erwartete Anzahl von Einträgen)/Ausführung des DoLongRunningWork-Delegaten.

+2

Downvote? Kein Kommentar? Vielen Dank. – wageoghe

Verwandte Themen