.NET Threadpool的一點認識

說到.NET Threadpool我想你們都知道,只是平時比較零散,顧如今整理一下:html

一碼阻塞,萬碼等待:ASP.NET Core 同步方法調用異步方法「死鎖」的真相算法

.NET Threadpool starvation, and how queuing makes it worse緩存

New and Improved CLR 4 Thread Pool Engine併發

因此本文主要是驗證和這裏這幾個文章app

Threadpool queue

當您調用ThreadPool.QueueUserWorkItem時,就是想象一個全局隊列,其中工做項(本質上是委託)在全局隊列中排隊,多個線程在一個隊列中選擇它們。先進先出順序。異步

左側的圖像顯示了主程序線程,由於它建立了一個工做項; 第二個圖像顯示代碼排隊3個工做項後的全局線程池隊列; 第三個圖像顯示了來自線程池的2個線程,它們抓取了2個工做項並執行它們。若是在這些工做項的上下文中(即來自委託中的執行代碼),爲CLR線程池建立了更多的工做項,它們最終會出如今全局隊列中(參見右圖),而且生命仍在繼續。async

在CLR 4中,線程池引擎已對其進行了一些改進(它在CLR的每一個版本中都進行了積極的調整),而且這些改進中的一部分是在使用新的System.Threading.Tasks時能夠實現的一些性能提高。建立和啓動一個Task(傳遞一個委託),至關於在ThreadPool上調用QueueUserWorkItem。經過基於任務的API使用時可視化CLR線程池的一種方法是,除了單個全局隊列以外,線程池中的每一個線程都有本身的本地隊列oop

正如一般的線程池使用同樣,主程序線程能夠建立將在全局隊列(例如Task1和Task2)上排隊的任務,而且線程將一般以FIFO方式獲取這些任務。事情分歧的是,在執行任務的上下文中建立的任何新任務(例如,Task2,Task23)最終在該線程池線程的本地隊列上性能

所以,從圖片中進一步提高,讓咱們假設Task2還建立了另外兩個任務,例如Task4和Task5。ui

任務按預期結束在本地隊列上,可是線程選擇在完成當前任務(即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,所以再次使用全局隊列

因爲使用全局隊列引發的優先級,咱們添加的線程越多,咱們對系統施加的壓力就越大,當使用本地隊列(代碼的第二個版本)時,新生成的線程將從其餘線程的本地隊列中選擇項目,由於全局隊列爲空。所以,新線程有助於減輕系統壓力。

相關文章
相關標籤/搜索