.NET Threadpool的一點認識
說到.NET Threadpool我想大家都知道,只是平時比較零散,顧現在整理一下:
一碼阻塞,萬碼等待:ASP.NET Core 同步方法呼叫非同步方法“死鎖”的真相
.NET Threadpool starvation, and how queuing makes it worse
New and Improved CLR 4 Thread Pool Engine
所以本文主要是驗證和這裡這幾個文章
Threadpool queue
當您呼叫ThreadPool.QueueUserWorkItem時,就是想象一個全域性佇列,其中工作項(本質上是委託)在全域性佇列中排隊,多個執行緒在一個佇列中選擇它們。先進先出順序。
左側的影象顯示了主程式執行緒,因為它建立了一個工作項; 第二個影象顯示程式碼排隊3個工作項後的全域性執行緒池佇列; 第三個影象顯示了來自執行緒池的2個執行緒,它們抓取了2個工作項並執行它們。如果在這些工作項的上下文中(即來自委託中的執行程式碼),為CLR執行緒池建立了更多的工作項,它們最終會出現在全域性佇列中(參見右圖),並且生命仍在繼續。
在CLR 4中,執行緒池引擎已對其進行了一些改進(它在CLR的每個版本中都進行了積極的調整),並且這些改進中的一部分是在使用新的System.Threading.Tasks時可以實現的一些效能提升。建立和啟動一個Task(傳遞一個委託),相當於在ThreadPool上呼叫QueueUserWorkItem。通過基於任務的API使用時視覺化CLR執行緒池的一種方法是,除了單個全域性佇列之外,執行緒池中的每個執行緒都有自己的本地佇列
正如通常的執行緒池使用一樣,主程式執行緒可以建立將在全域性佇列(例如Task1和Task2)上排隊的任務,並且執行緒將通常以FIFO方式獲取這些任務。事情分歧的是,在執行任務的上下文中建立的任何新任務(例如,Task2,Task23)最終在該執行緒池執行緒的本地佇列上。
因此,從圖片中進一步提升,讓我們假設Task2還建立了另外兩個任務,例如Task4和Task5。
任務按預期結束在本地佇列上,但是執行緒選擇在完成當前任務(即Task2)時執行哪個任務?最初令人驚訝的答案是它可能是Task5,它是排隊的最後一個 - 換句話說,LIFO演算法可以用於本地佇列。在大多數情況下,佇列中最後建立的任務所需的資料在快取中仍然很熱,因此將其拉下並執行它是有意義的。顯然,這意味著順序沒有承諾,但為了更好的表現,放棄了某種程度的公平。
其他工作執行緒完成Task1然後轉到其本地佇列並發現它為空; 然後它進入全域性佇列並發現它為空。我們不希望它閒置在那裡,所以發生了一件美妙的事情:偷工作。該執行緒進入另一個執行緒的本地佇列並“竊取”一個任務並執行它!這樣我們就可以保持所有核心的繁忙,這有助於實現細粒度並行負載平衡目標。在上圖中,注意“竊取”以FIFO方式發生,這也是出於地方原因的好處(其資料在快取中會很冷)。此外,在許多分而治之的場景中,之前生成的任務可能會產生更多的工作(例如Task6),這些工作現在最終會在另一個執行緒的佇列中結束,從而減少頻繁的竊取。
執行緒池有 n+1 個佇列,每個執行緒有自己的本地佇列(n),整個執行緒池有一個全域性佇列(1)。每個執行緒接活(從佇列中取出任務執行)的順序是這樣的:先從自己的本地佇列中找活 -> 如果本地佇列為空,則從全域性佇列中找活 -> 如果全域性佇列為空,則從其他執行緒的本地佇列中搶活。
TASK
先看以下4個demo
1:如果你執行程式,你會發現它在控制檯中設法顯示“Ended”幾次,然後就沒有任何事情發生了,就像假死了
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Starvation
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Environment.ProcessorCount);
ThreadPool.SetMinThreads(8, 8);
Task.Factory.StartNew( Producer,TaskCreationOptions.None);
Console.ReadLine();
}
static void Producer()
{
while (true)
{
Process();
Thread.Sleep(200);
}
}
static async Task Process()
{
await Task.Yield();
var tcs = new TaskCompletionSource<bool>();
Task.Run(() =>
{
Thread.Sleep(1000);
tcs.SetResult(true);
});
tcs.Task.Wait();
Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());
}
}
}
2:刪除Task.Yield並在Producer中手動啟動新任務。應用程式最初有點掙扎,直到執行緒池足夠增長。然後我們有一個穩定的訊息流,並且執行緒數是穩定的
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Starvation
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Environment.ProcessorCount);
ThreadPool.SetMinThreads(8, 8);
Task.Factory.StartNew( Producer,TaskCreationOptions.None);
Console.ReadLine();
}
static void Producer()
{
while (true)
{
// Creating a new task instead of just calling Process
// Needed to avoid blocking the loop since we removed the Task.Yield
Task.Factory.StartNew(Process);
Thread.Sleep(200);
}
}
static async Task Process()
{
// Removed the Task.Yield
var tcs = new TaskCompletionSource<bool>();
Task.Run(() =>
{
Thread.Sleep(1000);
tcs.SetResult(true);
});
tcs.Task.Wait();
Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());
}
}
}
3:工作程式碼但在其自己的執行緒中啟動Producer會怎麼樣,執行效果和1相似,有假死的效果, 但是感覺比1 好點
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Starvation
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Environment.ProcessorCount);
ThreadPool.SetMinThreads(8, 8);
Task.Factory.StartNew( Producer, TaskCreationOptions.LongRunning); // Start in a dedicated thread
Console.ReadLine();
}
static void Producer()
{
while (true)
{
Process();
Thread.Sleep(200);
}
}
static async Task Process()
{
await Task.Yield();
var tcs = new TaskCompletionSource<bool>();
Task.Run(() =>
{
Thread.Sleep(1000);
tcs.SetResult(true);
});
tcs.Task.Wait();
Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());
}
}
}
4.Producer放回執行緒池執行緒,但在啟動Process任務時使用PreferFairness標誌,再次遇到第一種情況:應用程式鎖定,執行緒數無限增加。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Starvation
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Environment.ProcessorCount);
ThreadPool.SetMinThreads(8, 8);
Task.Factory.StartNew(Producer, TaskCreationOptions.None);
Console.ReadLine();
}
static void Producer()
{
while (true)
{
Task.Factory.StartNew(Process, TaskCreationOptions.PreferFairness); // Using PreferFairness
Thread.Sleep(200);
}
}
static async Task Process()
{
var tcs = new TaskCompletionSource<bool>();
Task.Run(() =>
{
Thread.Sleep(1000);
tcs.SetResult(true);
});
tcs.Task.Wait();
Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());
}
}
}
執行緒挑選專案排隊的規則很簡單:
-
該項將被排入全域性佇列:
-
如果排隊專案的執行緒不是執行緒池執行緒
-
如果它使用ThreadPool.QueueUserWorkItem / ThreadPool.UnsafeQueueUserWorkItem
-
如果它使用Task.Factory.StartNew和TaskCreationOptions.PreferFairness標誌
-
如果它在預設任務排程程式上使用Task.Yield
-
-
在幾乎所有其他情況下,該項將被排入執行緒的本地佇列
每當執行緒池執行緒空閒時,它將開始檢視其本地佇列,並以LIFO順序對專案進行出列。如果本地佇列為空,則執行緒將檢視全域性佇列並以FIFO順序出列。如果全域性佇列也為空,則執行緒將檢視其他執行緒的本地佇列並以FIFO順序出列(以減少與佇列所有者的爭用,該佇列以LIFO順序出列)。
在程式碼的所有變體中,Thread.Sleep(1000)在本地佇列中排隊,因為Process總是線上程池執行緒中執行。但在某些情況下,我們將Process排入全域性佇列,而將其他佇列放入本地佇列:
-
在程式碼的第一個版本中,我們使用Task.Yield,它排隊到全域性佇列
-
在第二個版本中,我們使用Task.Factory.StartNew,它排隊到本地佇列
-
在第三個版本中,我們將Producer執行緒更改為不使用執行緒池,因此Task.Factory.StartNew排隊到全域性佇列
-
在第四個版本中,Producer再次是一個執行緒池執行緒,但我們在將Process 排入佇列時使用TaskCreationOptions.PreferFairness,因此再次使用全域性佇列
由於使用全域性佇列引起的優先順序,我們新增的執行緒越多,我們對系統施加的壓力就越大,當使用本地佇列(程式碼的第二個版本)時,新生成的執行緒將從其他執行緒的本地佇列中選擇專案,因為全域性佇列為空。因此,新執行緒有助於減輕系統壓力。