C#多線程編程系列(四)- 使用線程池

来源:https://www.cnblogs.com/InCerry/archive/2018/08/06/9432804.html
-Advertisement-
Play Games

[TOC] 1.1 簡介 在本章中,主要介紹 線程池(ThreadPool) 的使用;在C 中它叫 ,在使用線程池之前首先我們得明白一個問題,那就是為什麼要使用線程池。其主要原因是 創建一個線程的代價是昂貴的 ,創建一個線程會消耗很多的系統資源。 那麼線程池是如何解決這個問題的呢?線程池在初始時會自 ...


目錄



1.1 簡介

在本章中,主要介紹線程池(ThreadPool)的使用;在C#中它叫System.Threading.ThreadPool,在使用線程池之前首先我們得明白一個問題,那就是為什麼要使用線程池。其主要原因是創建一個線程的代價是昂貴的,創建一個線程會消耗很多的系統資源。

那麼線程池是如何解決這個問題的呢?線程池在初始時會自動創建一定量的線程供程式調用,使用時,開發人員並不直接分配線程,而是將需要做的工作放入線程池工作隊列中,由線程池分配已有的線程進行處理,等處理完畢後線程不是被銷毀,而是重新回到線程池中,這樣節省了創建線程的開銷。

但是在使用線程池時,需要註意以下幾點,這將非常重要。

  • 線程池不適合處理長時間運行的作業,或者處理需要與其它線程同步的作業。
  • 避免將線程池中的工作線程分配給I/O首先的任務,這種任務應該使用TPL模型。
  • 如非必須,不要手動設置線程池的最小線程數和最大線程數,CLR會自動的進行線程池的擴張和收縮,手動干預往往讓性能更差。

1.2 線上程池中調用委托

本節展示的是如何線上程池中如何非同步的執行委托,然後將介紹一個叫非同步編程模型(Asynchronous Programming Model,簡稱APM)的非同步編程方式。

在本節及以後,為了降低代碼量,在引用程式集聲明位置預設添加了using static System.Consoleusing static System.Threading.Thead聲明,這樣聲明可以讓我們在程式中少些一些意義不大的調用語句。

演示代碼如下所示,使用了普通創建線程和APM方式來執行同一個任務。

static void Main(string[] args)
{
    int threadId = 0;

    RunOnThreadPool poolDelegate = Test;

    var t = new Thread(() => Test(out threadId));
    t.Start();
    t.Join();

    WriteLine($"手動創建線程 Id: {threadId}");

    // 使用APM方式 進行非同步調用  非同步調用會使用線程池中的線程
    IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "委托非同步調用");
    r.AsyncWaitHandle.WaitOne();

    // 獲取非同步調用結果
    string result = poolDelegate.EndInvoke(out threadId, r);

    WriteLine($"Thread - 線程池工作線程Id: {threadId}");
    WriteLine(result);

    Console.ReadLine();
}

// 創建帶一個參數的委托類型
private delegate string RunOnThreadPool(out int threadId);

private static void Callback(IAsyncResult ar)
{
    WriteLine("Callback - 開始運行Callback...");
    WriteLine($"Callback - 回調傳遞狀態: {ar.AsyncState}");
    WriteLine($"Callback - 是否為線程池線程: {CurrentThread.IsThreadPoolThread}");
    WriteLine($"Callback - 線程池工作線程Id: {CurrentThread.ManagedThreadId}");
}

private static string Test(out int threadId)
{
    string isThreadPoolThread = CurrentThread.IsThreadPoolThread ? "ThreadPool - ": "Thread - ";

    WriteLine($"{isThreadPoolThread}開始運行...");
    WriteLine($"{isThreadPoolThread}是否為線程池線程: {CurrentThread.IsThreadPoolThread}");
    Sleep(TimeSpan.FromSeconds(2));
    threadId = CurrentThread.ManagedThreadId;
    return $"{isThreadPoolThread}線程池工作線程Id: {threadId}";
}

運行結果如下圖所示,其中以Thread開頭的為手動創建的線程輸出的信息,而TheadPool為開始線程池任務輸出的信息,Callback為APM模式運行任務結束後,執行的回調方法,可以清晰的看到,Callback的線程也是線程池的工作線程。

1533523675523

在上文中,使用BeginOperationName/EndOperationName方法和.Net中的IAsyncResult對象的方式被稱為非同步編程模型(或APM模式),這樣的方法被稱為非同步方法。使用委托的BeginInvoke方法來運行該委托,BeginInvoke接收一個回調函數,該回調函數會在任務處理完成後背調用,並且可以傳遞一個用戶自定義的狀態給回調函數。

現在這種APM編程方式用的越來越少了,更推薦使用任務並行庫(Task Parallel Library,簡稱TPL)來組織非同步API。

1.3 向線程池中放入非同步操作

本節將介紹如何將非同步操作放入線程池中執行,並且如何傳遞參數給線程池中的線程。本節中主要用到的是ThreadPool.QueueUserWorkItem()方法,該方法可將需要運行的任務通過委托的形式傳遞給線程池中的線程,並且允許傳遞參數。

使用比較簡單,演示代碼如下所示。演示了線程池使用中如何傳遞方法和參數,最後需要註意的是使用了Lambda表達式和它的閉包機制。

static void Main(string[] args)
{
    const int x = 1;
    const int y = 2;
    const string lambdaState = "lambda state 2";

    // 直接將方法傳遞給線程池
    ThreadPool.QueueUserWorkItem(AsyncOperation);
    Sleep(TimeSpan.FromSeconds(1));

    // 直接將方法傳遞給線程池 並且 通過state傳遞參數
    ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
    Sleep(TimeSpan.FromSeconds(1));

    // 使用Lambda表達式將任務傳遞給線程池 並且通過 state傳遞參數
    ThreadPool.QueueUserWorkItem(state =>
    {
        WriteLine($"Operation state: {state}");
        WriteLine($"工作線程 id: {CurrentThread.ManagedThreadId}");
        Sleep(TimeSpan.FromSeconds(2));
    }, "lambda state");

    // 使用Lambda表達式將任務傳遞給線程池 通過 **閉包** 機制傳遞參數
    ThreadPool.QueueUserWorkItem(_ =>
    {
        WriteLine($"Operation state: {x + y}, {lambdaState}");
        WriteLine($"工作線程 id: {CurrentThread.ManagedThreadId}");
        Sleep(TimeSpan.FromSeconds(2));
    }, "lambda state");

    ReadLine();
}

private static void AsyncOperation(object state)
{
    WriteLine($"Operation state: {state ?? "(null)"}");
    WriteLine($"工作線程 id: {CurrentThread.ManagedThreadId}");
    Sleep(TimeSpan.FromSeconds(2));
}

運行結果如下圖所示。

1533537253353

1.4 線程池與並行度

在本節中,主要是使用普通創建線程和使用線程池內的線程在任務量比較大的情況下有什麼區別,我們模擬了一個場景,創建了很多不同的線程,然後分別使用普通創建線程方式和線程池方式看看有什麼不同。

static void Main(string[] args)
{
    const int numberOfOperations = 500;
    var sw = new Stopwatch();
    sw.Start();
    UseThreads(numberOfOperations);
    sw.Stop();
    WriteLine($"使用線程執行總用時: {sw.ElapsedMilliseconds}");

    sw.Reset();
    sw.Start();
    UseThreadPool(numberOfOperations);
    sw.Stop();
    WriteLine($"使用線程池執行總用時: {sw.ElapsedMilliseconds}");

    Console.ReadLine();
}

static void UseThreads(int numberOfOperations)
{
    using (var countdown = new CountdownEvent(numberOfOperations))
    {
        WriteLine("通過創建線程調度工作");
        for (int i = 0; i < numberOfOperations; i++)
        {
            var thread = new Thread(() =>
            {
                Write($"{CurrentThread.ManagedThreadId},");
                Sleep(TimeSpan.FromSeconds(0.1));
                countdown.Signal();
            });
            thread.Start();
        }
        countdown.Wait();
        WriteLine();
    }
}

static void UseThreadPool(int numberOfOperations)
{
    using (var countdown = new CountdownEvent(numberOfOperations))
    {
        WriteLine("使用線程池開始工作");
        for (int i = 0; i < numberOfOperations; i++)
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {
                Write($"{CurrentThread.ManagedThreadId},");
                Sleep(TimeSpan.FromSeconds(0.1));
                countdown.Signal();
            });
        }
        countdown.Wait();
        WriteLine();
    }
}

執行結果如下,可見使用原始的創建線程執行,速度非常快。只花了2秒鐘,但是創建了500多個線程,而使用線程池相對來說比較慢,花了9秒鐘,但是只創建了很少的線程,為操作系統節省了線程和記憶體空間,但花了更多的時間。

1533540572298

1.5 實現一個取消選項

在之前的文章中有提到,如果需要終止一個線程的執行,那麼可以使用Abort()方法,但是有諸多的原因並不推薦使用Abort()方法。

這裡推薦的方式是使用協作式取消(cooperative cancellation),這是一種可靠的技術來安全取消不再需要的任務。其主要用到CancellationTokenSourceCancellationToken兩個類,具體用法見下麵演示代碼。

以下延時代碼主要是實現了使用CancellationTokenCancellationTokenSource來實現任務的取消。但是任務取消後可以進行三種操作,分別是:直接返回、拋出ThrowIfCancellationRequesed異常和執行回調。詳細請看代碼。

static void Main(string[] args)
{
    // 使用CancellationToken來取消任務  取消任務直接返回
    using (var cts = new CancellationTokenSource())
    {
        CancellationToken token = cts.Token;
        ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token));
        Sleep(TimeSpan.FromSeconds(2));
        cts.Cancel();
    }

    // 取消任務 拋出 ThrowIfCancellationRequesed 異常
    using (var cts = new CancellationTokenSource())
    {
        CancellationToken token = cts.Token;
        ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token));
        Sleep(TimeSpan.FromSeconds(2));
        cts.Cancel();
    }

    // 取消任務 並 執行取消後的回調函數
    using (var cts = new CancellationTokenSource())
    {
        CancellationToken token = cts.Token;
        token.Register(() => { WriteLine("第三個任務被取消,執行回調函數。"); });
        ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token));
        Sleep(TimeSpan.FromSeconds(2));
        cts.Cancel();
    }

    ReadLine();
}

static void AsyncOperation1(CancellationToken token)
{
    WriteLine("啟動第一個任務.");
    for (int i = 0; i < 5; i++)
    {
        if (token.IsCancellationRequested)
        {
            WriteLine("第一個任務被取消.");
            return;
        }
        Sleep(TimeSpan.FromSeconds(1));
    }
    WriteLine("第一個任務運行完成.");
}

static void AsyncOperation2(CancellationToken token)
{
    try
    {
        WriteLine("啟動第二個任務.");

        for (int i = 0; i < 5; i++)
        {
            token.ThrowIfCancellationRequested();
            Sleep(TimeSpan.FromSeconds(1));
        }
        WriteLine("第二個任務運行完成.");
    }
    catch (OperationCanceledException)
    {
        WriteLine("第二個任務被取消.");
    }
}

static void AsyncOperation3(CancellationToken token)
{
    WriteLine("啟動第三個任務.");
    for (int i = 0; i < 5; i++)
    {
        if (token.IsCancellationRequested)
        {
            WriteLine("第三個任務被取消.");
            return;
        }
        Sleep(TimeSpan.FromSeconds(1));
    }
    WriteLine("第三個任務運行完成.");
}

運行結果如下所示,符合預期結果。

1533547890589

1.6 線上程池中使用等待事件處理器及超時

本節將介紹如何線上程池中使用等待任務和如何進行超時處理,其中主要用到ThreadPool.RegisterWaitForSingleObject()方法,該方法允許傳入一個WaitHandle對象,和需要執行的任務、超時時間等。通過使用這個方法,可完成線程池情況下對超時任務的處理。

演示代碼如下所示,運行了兩次使用ThreadPool.RegisterWaitForSingleObject()編寫超時代碼的RunOperations()方法,但是所傳入的超時時間不同,所以造成一個必然超時和一個不會超時的結果。

static void Main(string[] args)
{
    // 設置超時時間為 5s WorkerOperation會延時 6s 肯定會超時
    RunOperations(TimeSpan.FromSeconds(5));

    // 設置超時時間為 7s 不會超時
    RunOperations(TimeSpan.FromSeconds(7));
}

static void RunOperations(TimeSpan workerOperationTimeout)
{
    using (var evt = new ManualResetEvent(false))
    using (var cts = new CancellationTokenSource())
    {
        WriteLine("註冊超時操作...");
        // 傳入同步事件  超時處理函數  和 超時時間
        var worker = ThreadPool.RegisterWaitForSingleObject(evt
            , (state, isTimedOut) => WorkerOperationWait(cts, isTimedOut)
            , null
            , workerOperationTimeout
            , true);

        WriteLine("啟動長時間運行操作...");
        ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt));

        Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2)));

        // 取消註冊等待的操作
        worker.Unregister(evt);

        ReadLine();
    }
}

static void WorkerOperation(CancellationToken token, ManualResetEvent evt)
{
    for (int i = 0; i < 6; i++)
    {
        if (token.IsCancellationRequested)
        {
            return;
        }
        Sleep(TimeSpan.FromSeconds(1));
    }
    evt.Set();
}

static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut)
{
    if (isTimedOut)
    {
        cts.Cancel();
        WriteLine("工作操作超時並被取消.");
    }
    else
    {
        WriteLine("工作操作成功.");
    }
}

運行結果如下圖所示,與預期結果相符。

1533556752088

1.7 使用計時器

計時器是FCL提供的一個類,叫System.Threading.Timer,可要結果與創建周期性的非同步操作。該類使用比較簡單。

以下的演示代碼使用了定時器,並設置了定時器延時啟動時間和周期時間。

static void Main(string[] args)
{
    WriteLine("按下回車鍵,結束定時器...");
    DateTime start = DateTime.Now;

    // 創建定時器
    _timer = new Timer(_ => TimerOperation(start), null
        , TimeSpan.FromSeconds(1)
        , TimeSpan.FromSeconds(2));
    try
    {
        Sleep(TimeSpan.FromSeconds(6));

        _timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4));

        ReadLine();
    }
    finally
    {
        //實現了IDispose介面  要及時釋放
        _timer.Dispose();
    }
}

static Timer _timer;

static void TimerOperation(DateTime start)
{
    TimeSpan elapsed = DateTime.Now - start;
    WriteLine($"離 {start} 過去了 {elapsed.Seconds} 秒. " +
              $"定時器線程池 線程 id: {CurrentThread.ManagedThreadId}");
}

運行結果如下所示,可見定時器根據所設置的周期時間迴圈的調用TimerOperation()方法。

1533557619004

1.8 使用BackgroundWorker組件

本節主要介紹BackgroundWorker組件的使用,該組件實際上被用於Windows窗體應用程式(Windows Forms Application,簡稱 WPF)中,通過它實現的代碼可以直接與UI控制器交互,更加自認和好用。

演示代碼如下所示,使用BackgroundWorker來實現對數據進行計算,並且讓其支持報告工作進度,支持取消任務。

static void Main(string[] args)
{
    var bw = new BackgroundWorker();
    // 設置可報告進度更新
    bw.WorkerReportsProgress = true;
    // 設置支持取消操作
    bw.WorkerSupportsCancellation = true;

    // 需要做的工作
    bw.DoWork += Worker_DoWork;
    // 工作處理進度
    bw.ProgressChanged += Worker_ProgressChanged;
    // 工作完成後處理函數
    bw.RunWorkerCompleted += Worker_Completed;

    bw.RunWorkerAsync();

    WriteLine("按下 `C` 鍵 取消工作");
    do
    {
        if (ReadKey(true).KeyChar == 'C')
        {
            bw.CancelAsync();
        }

    }
    while (bw.IsBusy);
}

static void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    WriteLine($"DoWork 線程池 線程 id: {CurrentThread.ManagedThreadId}");
    var bw = (BackgroundWorker)sender;
    for (int i = 1; i <= 100; i++)
    {
        if (bw.CancellationPending)
        {
            e.Cancel = true;
            return;
        }
        if (i % 10 == 0)
        {
            bw.ReportProgress(i);
        }

        Sleep(TimeSpan.FromSeconds(0.1));
    }

    e.Result = 42;
}

static void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    WriteLine($"已完成{e.ProgressPercentage}%. " +
              $"處理線程 id: {CurrentThread.ManagedThreadId}");
}

static void Worker_Completed(object sender, RunWorkerCompletedEventArgs e)
{
    WriteLine($"完成線程池線程 id: {CurrentThread.ManagedThreadId}");
    if (e.Error != null)
    {
        WriteLine($"異常 {e.Error.Message} 發生.");
    }
    else if (e.Cancelled)
    {
        WriteLine($"操作已被取消.");
    }
    else
    {
        WriteLine($"答案是 : {e.Result}");
    }
}

運行結果如下所示。

1533558846212

在本節中,使用了C#中的另外一個語法,叫事件(event)。當然這裡的事件不同於之前線上程同步章節中提到的事件,這裡是觀察者設計模式的體現,包括事件源、訂閱者和事件處理程式。因此,除了非同步APM模式意外,還有基於事件的非同步模式(Event-based Asynchronous Pattern,簡稱 EAP)

參考書籍

本文主要參考了以下幾本書,在此對這些作者表示由衷的感謝你們提供了這麼好的資料。

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》
  5. 《C#多線程編程實戰》

源碼下載點擊鏈接 示例源碼下載

筆者水平有限,如果錯誤歡迎各位批評指正!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Tesseract是什麼 OCR即光學字元識別,是指通過電子設備掃描紙上的列印的字元,然後翻譯成電腦文字的過程。也就是說通過輸入圖片,經過識別引擎,去識別圖片上的文字。Tesseract是一種適用於各種操作系統的光學字元識別引擎,最早是hp公司的軟體,2005年開源,2006年後由google一直 ...
  • # 主程式運行 import time from guanli import GuanLi from atm import ATM from user import User def main(): guanli = GuanLi()# 創建一個管理對象 guanli.welcome() # 歡迎界... ...
  • public final class String extends Object 1、用final修飾的最終類,是代表字元串的類; 2、字元串在底層是以字元數組的形式存儲; 3、任何的字元串都是String的對象 4、字元串常量可以共用(下圖) String s1 = “ab”; String s2 ...
  • 1 切片初始化 Out: 2 切片長度與容量 切片的長度就是它所包含的元素個數。 切片的容量是從它的第一個元素開始數,到其底層數組元素末尾的個數 Out: 3 賦值與傳參 Out 2~4 一個數組變數表示整個數組,它不是指向第一個元素的指針(不像 C 語言的數組)因此數組名通過%p 無法列印地址。 ...
  • Apache Ranger是什麼,它是一個為Hadoop平臺提供了全面的數據安全訪問控制及監控的集中式管理框架,Apache頂級項目。不廢話了,其實本篇沒那麼高大上,就是一步步教你如何將Ranger源碼導入到IDEA,並運行調試其web模塊。 ...
  • 微信是時下很火的一大社交工具,據不完全統計,目前全國微信譽戶高達數十億。這樣的數據足夠證明這一軟體的運用普遍水平之高。在將來隨著互聯網技術的日益普及,也將會有更多的人參加到運用微信的隊伍中。微信雖然給人們的生活帶來了快捷與便當,但是微信被破解的事情也時有發作,而這樣的事情也常常呈現在我對象的身上。究 ...
  • 在生活中想必很多人都想要瞭解他人的微信中的一些內容。事實上要想瞭解他人微信中的一些內容的話,可以通過破解他人微信密碼來實現。那麼怎樣破解他人微信密碼不被髮現以及盜取他人微信密碼的步驟是什麼呢?想必有很多朋友想要瞭解這個問題,這不在此便悄悄的告訴大家一個破解他人微信密碼不被髮現的方法。 不到萬不得已的 ...
  • 首先大家要知道在瀏覽器上瀏覽虛擬主機,必須使用Hosts文件或功能變數名稱系統(DNS)實現主機名到IP地址的解析。在區域網中用Hosts文件或DNS都可以,在Internet上只能用DNS了。 1.當用戶輸入一個功能變數名稱以百度為例(www.baidu.com)。 2.首先會到C:\Windows\System ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...