Thread 和 ThreadPool 簡單梳理(C#)【並行程式設計系列】

2023-07-14 21:00:23

〇、前言

對於 Thread 和 ThreadPool 已經是元老級別的類了。Thread 是 C# 語言對執行緒物件的封裝,它從 .NET 1.0 版本就有了,然後 ThreadPool 是 .Net Framework 2.0 版本中出現的,都是相當成熟的存在。

當然,現在已經出現了 Task 和 PLinq 等更高效率的並行類,執行緒和執行緒池在實際開發中逐漸減少了,但是不能不知道他們的用法,因為總有需要對接的內容,別人用了你也得能看懂。

本文將結合範例,簡單介紹下 Thread 和 ThreadPool。

一、Thread 類

Thread 類的功能就是,建立和控制執行緒,設定其優先順序並獲取其狀態。

 下邊程式碼簡單範例說明下 Thread 的相關內容:

public static void Main()
{
    // (1)
    //var th1 = new Thread(ExecuteInForeground);
    //th1.Start();
    // (2)
    //var th2 = new Thread(ExecuteInForeground);
    //th2.IsBackground = true;
    //th2.Start();
    // (3)
    //ThreadPool.QueueUserWorkItem(ExecuteInForeground);
    Thread.Sleep(1000);
    // Console.WriteLine($"主執行緒 ({Thread.CurrentThread.ManagedThreadId}) 即將退出 執行 Join() 方法。。。");
    // th2.Join();
    Console.WriteLine($"主執行緒 ({Thread.CurrentThread.ManagedThreadId}) 即將退出。。。");
    //Console.ReadLine();
}
private static void ExecuteInForeground(object state)
{
    var sw = Stopwatch.StartNew();
    Console.WriteLine("執行緒 {0}: {1}, 優先順序: {2}",
                        Thread.CurrentThread.ManagedThreadId,
                        Thread.CurrentThread.ThreadState,
                        Thread.CurrentThread.Priority);
    do
    {
        Console.WriteLine("執行緒 {0}: 計時 {1:N2} 秒",
                            Thread.CurrentThread.ManagedThreadId,
                            sw.ElapsedMilliseconds / 1000.0);
        Thread.Sleep(500);
    } while (sw.ElapsedMilliseconds <= 5000);
    sw.Stop();
}

註釋部分三組執行緒啟動的結果如下三圖:

  

第 1 部分,是前臺執行緒,必須執行完畢,主執行緒才會退出,所以一直執行到 5s 之前。

第 2、3 部分,均為後臺執行緒,當主執行緒執行完成之時,無論是否執行完成直接中斷,所以只回圈了兩次就退出了。

關於 Join() 方法

程式碼中th2.Join()如果在後臺執行緒上執行,這結果如下圖,將會等待後臺執行緒完成後主執行緒才結束。

  

 二、ThreadPool 類

由於執行緒物件的建立時需要分配記憶體,GC 過程中銷燬物件,然後整合零散的記憶體塊,從而佔用 CPU 資源,會影響程式效能,所以 ThreadPool 誕生了。

  • 使用執行緒池,可以通過嚮應用程式提供由系統管理的工作執行緒池,來更有效的使用執行緒。
  • 執行緒池可以通過重用執行緒、控制執行緒數量等操作,減少頻繁建立和切換執行緒所帶來的開銷,從而提高響應速度。
  • 可直接使用執行緒池中空閒的執行緒,而不必等待執行緒的建立,方便管理執行緒。

注意,託管執行緒池中的執行緒是後臺執行緒,其 IsBackground 屬性為 true。

1、ThreadPool 的幾個屬性值

  • CompletedWorkItemCount:獲取迄今為止已處理的工作項數。
  • PendingWorkItemCount:獲取當前已加入處理佇列的工作項數。
  • ThreadCount:獲取當前存在的執行緒池執行緒數。

下面是一個關於執行緒池的幾個屬性值,以及開啟新的後臺執行緒並傳入引數的範例:

//存放要計算的數值的欄位
public static double num1 = -1;
public static double num2 = -1;
static void Main(string[] args)
{
    int workerThreads, completionPortThreads;
    // public static void GetMaxThreads (out int workerThreads, out int completionPortThreads);
    ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
    Console.WriteLine($"執行緒池中輔助執行緒的最大數目:{workerThreads}");
    Console.WriteLine($"執行緒池中非同步 I/O 執行緒的最大數目:{completionPortThreads}");
    Console.WriteLine();
    // public static void GetMinThreads(out int workerThreads, out int completionPortThreads);
    ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
    Console.WriteLine($"執行緒池根據需要建立的最少數量的輔助執行緒:{workerThreads}");
    Console.WriteLine($"執行緒池根據需要建立的最少數量的非同步 I/O 執行緒:{completionPortThreads}");
    Console.WriteLine();
    ThreadPool.SetMaxThreads(100, 15); // set 的值必須是 Min~Max 之間的值,否則會設定不成功
    ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
    Console.WriteLine($"set 執行緒池中輔助執行緒的最大數目:{workerThreads}");
    Console.WriteLine($"set 執行緒池中非同步 I/O 執行緒的最大數目:{completionPortThreads}");
    Console.WriteLine();

    // 命名引數 傳入後臺執行緒
    int num = 2;
    // 啟動第一個任務:計算x的8次方
    Console.WriteLine("啟動第一個任務:計算{0}的8次方.", num);
    ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc1), num);
    // 啟動第二個任務
    Console.WriteLine("啟動第二個任務:計算{0}的8次方", num);
    ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc2), num);
    // 等待兩個數值等完成計算
    while (num1 == -1 || num2 == -1) ;
    //列印計算結果
    Console.WriteLine($"{num} 的 8 次方為 {num1} {num2}");
    Console.ReadLine();
}
private static void TaskProc2(object state)
{
    Console.WriteLine($"TaskProc2-Thread-{Thread.CurrentThread.IsBackground}");
    num1 = Math.Pow(Convert.ToDouble(state), 8);
}
private static void TaskProc1(object state)
{
    num2 = Math.Pow(Convert.ToDouble(state), 8);
}

 輸出結果:

  

 2、由執行緒池生成一個可以取消的後臺執行緒

 如下程式碼,在沒有單擊確認鍵之前,程式會一直列印遞增數位,當收到回車指令後,cts.Cancel();被執行,後臺執行緒就取消成功了。

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    ThreadPool.QueueUserWorkItem(t => Counts(cts.Token, 1000));
    Console.WriteLine("Press Any Key to cancel the operation");
    Console.ReadLine();
    cts.Cancel();
    Console.ReadLine();
}
private static void Counts(CancellationToken token, int CountTo)
{
    for (int count = 0; count < CountTo; count++)
    {
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("Count is cancelled");
            break;
        }
        Console.WriteLine(count);
        Thread.Sleep(200);
    }
    Console.WriteLine("Count is stopped");
}

 結果如下圖:

  

 三、Thread 和 ThreadPool 效能比較

如下程式碼,分別執行 100 次,看最終需要的時間成本:

public static void Main()
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 100; i++)
    {
        Thread th = new Thread(() =>
        {
            int count = 0;
            count++;
        });
        th.Start();
    }
    sw.Stop();
    Console.WriteLine("執行建立執行緒所需要的時間為:" + sw.ElapsedMilliseconds);
    sw.Restart();
    for (int i = 0; i < 100; i++)
    {
        ThreadPool.QueueUserWorkItem(t =>
        {
            int count = 0;
            count++;
        });
    }
    sw.Stop();
    Console.WriteLine("執行執行緒池所需要花費的時間:" + sw.ElapsedMilliseconds);
    Console.ReadLine();
}

如下圖,明顯執行緒池效能更佳:

  

參考:https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.threadpool?view=net-7.0  

https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.thread?view=net-7.0  

C#(ThreadPool)執行緒池的詳解及使用範例.NET(C#) ThreadPool執行緒池的使用總結