[英]How to limit the number of active Tasks running via the Parallel Task Library?
[英]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種方法來執行並行操作。 要嘗試每個,只需取消注釋所需的“使用...”方法,重新編譯並運行。 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(和/或其他線程/任務構造)一起使用或者如何使用它的整個問題可能有其自身的問題。 也許我會發一個問題。 在此期間,我想知道你是否對此有任何想法(或者,我想知道你是否考慮過使用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.