1. 程式人生 > >.NET Threadpool的一點認識

.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.StartNewTaskCreationOptions.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,因此再次使用全域性佇列

由於使用全域性佇列引起的優先順序,我們新增的執行緒越多,我們對系統施加的壓力就越大,當使用本地佇列(程式碼的第二個版本)時,新生成的執行緒將從其他執行緒的本地佇列中選擇專案,因為全域性佇列為空。因此,新執行緒有助於減輕系統壓力。