簡體   English   中英

任務並行庫中的任務如何影響ActivityID?

[英]How do Tasks in the Task Parallel Library affect ActivityID?

在使用任務並行庫之前,我經常使用CorrelationManager.ActivityId來跟蹤多線程的跟蹤/錯誤報告。

ActivityId存儲在線程本地存儲中,因此每個線程都獲得自己的副本。 這個想法是當你啟動一個線程(活動)時,你分配一個新的ActivityId。 ActivityId將使用任何其他跟蹤信息寫入日志,從而可以單獨列出單個“活動”的跟蹤信息。 這對於WCF非常有用,因為ActivityId可以轉移到服務組件。

這是我正在談論的一個例子:

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

現在,通過TPL,我的理解是多個任務共享線程。 這是否意味着ActivityId很容易在任務中重新初始化(通過另一項任務)? 是否有新的機制來處理活動追蹤?

我進行了一些實驗,結果發現我的問題中的假設是錯誤的 - 使用TPL創建的多個任務不會同時在同一個線程上運行。

ThreadLocalStorage可以安全地與.NET 4.0中的TPL一起使用,因為一個線程一次只能由一個任務使用。

任務可以同時共享線程的假設是基於我在DotNetRocks上聽到的關於c#5.0采訪 (抱歉,我不記得它是哪個節目) - 所以我的問題可能(或可能不會)很快就會變得相關。

我的實驗啟動了許多任務,並記錄了運行了多少任務,花了多長時間以及消耗了多少線程。 如果有人想重復,代碼如下。

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

輸出(當然這取決於機器)是:

Completed 100 tasks in 23097 milliseconds
Used 23 threads

將taskCreationOpt更改為TaskCreationOptions.LongRunning會產生不同的結果:

Completed 100 tasks in 3458 milliseconds 
Used 100 threads

請原諒我發布這個作為答案,因為它不是你的問題的真正答案,但是,它與你的問題有關,因為它處理CorrelationManager行為和線程/任務/等。 我一直在尋找使用CorrelationManager的LogicalOperationStack (和StartLogicalOperation/StopLogicalOperation方法)在多線程場景中提供額外的上下文。

我拿了你的例子並稍微修改它以增加使用Parallel.For並行執行工作的能力。 另外,我使用StartLogicalOperation/StopLogicalOperation來括號(內部) DoLongRunningWork 從概念上講, DoLongRunningWork每次執行時都會執行以下操作:

DoLongRunningWork
  StartLogicalOperation
  Thread.Sleep(3000)
  StopLogicalOperation

我發現如果我將這些邏輯操作添加到您的代碼中(或多或少),所有邏輯操作都保持同步(始終是堆棧上預期的操作數,並且堆棧上的操作值始終為預期)。

在我自己的一些測試中,我發現並非總是這樣。 邏輯操作堆棧正在“損壞”。 我能想到的最好的解釋是,當“子”線程退出時,將CallContext信息“合並”回“父”線程上下文導致“舊”子線程上下文信息(邏輯操作)為“繼承“由另一個兄弟姐妹線程。

問題也可能與Parallel.For顯然使用主線程(至少在示例代碼中,如編寫)作為“工作線程”之一(或者在並行域中應該調用它們)之間的事實有關。 每當執行DoLongRunningWork時,就會啟動一個新的邏輯操作(在開始時)並停止(在結束時)(即,將其推送到LogicalOperationStack並從中彈出)。 如果主線程已經有效的邏輯操作,並且DoLongRunningWork在主線程上執行,則啟動新的邏輯操作,因此主線程的LogicalOperationStack現在具有兩個操作。 DoLongRunningWork的任何后續執行(只要DoLongRunningWork的這個“迭代”在主線程上執行)將(顯然)繼承主線程的LogicalOperationStack(現在它有兩個操作,而不僅僅是一個預期的操作)。

我花了很長時間才弄清楚為什么LogicalOperationStack的行為在我的示例中與我的示例的修改版本不同。 最后我看到在我的代碼中我將整個程序放在邏輯操作中,而在我的測試程序的修改版本中,我沒有。 這意味着在我的測試程序中,每次執行“工作”(類似於DoLongRunningWork)時,已經存在邏輯操作。 在我的測試程序的修改版本中,我沒有在邏輯操作中將整個程序括起來。

所以,當我修改你的測試程序以在邏輯操作中包含整個程序時如果我使用Parallel.For,我遇到了完全相同的問題。

使用上面的概念模型,這將成功運行:

Parallel.For
  DoLongRunningWork
    StartLogicalOperation
    Sleep(3000)
    StopLogicalOperation

雖然這最終會因為LogicalOperationStack顯然不同步而斷言:

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

這是我的示例程序。 它類似於你的,因為它有一個DoLongRunningWork方法來操作ActivityId以及LogicalOperationStack。 我也有兩種踢DoLongRunningWork的方式。 一種風味使用任務一使用Parallel.For。 還可以執行每種風格,使得整個並行操作被包含在邏輯操作中或不包含在邏輯操作中。 因此,總共有4種方法來執行並行操作。 要嘗試每個,只需取消注釋所需的“使用...”方法,重新編譯並運行。 UseTasksUseTasks(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(和/或其他線程/任務構造)一起使用或者如何使用它的整個問題可能有其自身的問題。 也許我會發一個問題。 在此期間,我想知道你是否對此有任何想法(或者,我想知道你是否考慮過使用LogicalOperationStack,因為ActivityId似乎是安全的)。

[編輯]

有關使用LogicalOperationStack和/或CallContext.LogicalSetData以及各種Thread / ThreadPool / Task / Parallel結構的更多信息,請參閱我對此問題的回答。

另請參閱我在這里關於LogicalOperationStack和Parallel擴展的問題: CorrelationManager.LogicalOperationStack是否與Parallel.For,Tasks,Threads等兼容

最后,請參閱Microsoft的Parallel Extensions論壇上的問題: http//social.msdn.microsoft.com/Forums/en-US/parallelextensions/thread/7c5c3051-133b-4814-9db0-fc0039b4f9d9

在我的測試中,當使用Parallel.For或Parallel.Invoke時,看起來Trace.CorrelationManager.LogicalOperationStack可能會損壞。如果您在主線程中啟動邏輯操作,然后在委托中啟動/停止邏輯操作。 在我的測試中(參見上面兩個鏈接中的任何一個),當DoLongRunningWork正在執行時,LogicalOperationStack應該總是有2個條目(如果我在使用各種技術踢DoLongRunningWork之前在主線程中啟動邏輯操作)。 因此,“損壞”是指LogicalOperationStack最終會有超過2個條目。

據我所知,這可能是因為Parallel.For和Parallel.Invoke使用主線程作為執行DoLongRunningWork操作的“工作”線程之一。

使用存儲在CallContext.LogicalSetData中的堆棧來模仿LogicalOperationStack的行為(類似於通過CallContext.SetData存儲的log4net的LogicalThreadContext.Stacks)會產生更糟糕的結果。 如果我使用這樣的堆棧來維護上下文,它幾乎在我主線程中有“邏輯操作”並且每次迭代中都有邏輯操作的所有場景中都會被破壞(即沒有預期的條目數) /執行DoLongRunningWork委托。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM