C# 多線程學習筆記 - 3

来源:https://www.cnblogs.com/myzony/archive/2019/02/15/10381575.html
-Advertisement-
Play Games

本文主要針對 GKarch 相關文章留作筆記,僅在原文基礎上記錄了自己的理解與摘抄部分片段。 遵循原作者的 "CC 3.0 協議" 。 如果想要瞭解更加詳細的文章信息內容,請訪問下列地址進行學習。 原文章地址: "https://blog.gkarch.com/threading/part3.htm ...


本文主要針對 GKarch 相關文章留作筆記,僅在原文基礎上記錄了自己的理解與摘抄部分片段。
遵循原作者的 CC 3.0 協議
如果想要瞭解更加詳細的文章信息內容,請訪問下列地址進行學習。

原文章地址:https://blog.gkarch.com/threading/part3.html

一、基於事件的非同步模式

  1. 基於事件的非同步模式 (event-based asynchronous pattern) 提供了簡單的方式,讓類型提供多線程的能力而不需要顯式啟動線程。

    • 協作取消模型。
    • 工作線程完成時安全更新 UI 的能力。
    • 轉發異常到完成事件。
  2. EAP 僅是一個模式,需要開發人員自己實現。

  3. EAP 一般會提供一組成員,在其內部管理工作線程,例如 WebClient 類型就使用的 EAP 模式進行設計。

    // 下載數據的同步版本。
    public byte[] DownloadData (Uri address);
    // 下載數據的非同步版本。
    public void DownloadDataAsync (Uri address);
    // 下載數據的非同步版本,支持傳入 token 標識任務。
    public void DownloadDataAsync (Uri address, object userToken);
    // 完成時候的事件,當任務取消,出現異常或者更新 UI 操作都可以才該事件內部進行操作。
    public event DownloadDataCompletedEventHandler DownloadDataCompleted;
    
    public void CancelAsync (object userState);  // 取消一個操作
    public bool IsBusy { get; }                  // 指示是否仍在運行
  4. 通過 Task 可以很方便的實現 EAP 模式類似的功能。

二、BackgroundWorker

  1. BackgroundWorker 是一個通用的 EAP 實現,提供了下列功能。
    • 協作取消模型。
    • 工作線程完成時安全更新 UI 的能力。
    • 轉發異常到完成事件。
    • 報告工作進度的協議。
  2. BackgroundWorker 使用線程池來創建線程,所以不應該在 BackgroundWorker 的線程上調用 Abort() 方法。

2.1 使用方法

  1. 實例化 BackgroundWorker 對象,並且掛接 DoWork 事件。

  2. 調用 RunWorkerAsync() 可以傳遞一個 object 參數,以上則是 BackgroundWorker 的最簡使用方法。

  3. 可以為 BackgroundWorker 對象掛接 RunWorkerCompleted 事件,在該事件內部可以對工作線程執行後的異常與結果進行檢查,並且可以直接在該事件內部安全地更新 UI 組件。

  4. 如果需要支持取消功能,則需要將 WorkerSupportsCancellation 屬性置為 true。這樣在 DoWork() 事件當中就可通過檢查對象的 CancellationPending 屬性來確定是否被取消,如果是則將 Cancel 置為 true 並結束工作事件。

  5. 調用 CancelAsync 來請求取消。

  6. 開發人員不一定需要在 CancellationPendingtrue 時才取消任務,隨時可以通過將 Cancel 置為 true 來終止任務。

  7. 如果需要添加工作進度報告,則需要將 WorkerReportsProgress 屬性置為 true,併在 DoWork 事件中周期性地調用 ReportProcess() 方法來報告工作進度。同時掛接 ProgressChanged 事件,在其內部可以安全地更新 UI 組件,例如設置進度條 Value 值。

  8. 下列代碼即是上述功能的完整實現。

    class Program
    {
     static void Main()
     {
         var backgroundTest = new BackgroundWorkTest();
         backgroundTest.Run();
         Console.ReadLine();
     }
    }
    
    public class BackgroundWorkTest
    {
     private readonly BackgroundWorker _bw = new BackgroundWorker();
    
     public BackgroundWorkTest()
     {
         // 綁定工作事件
         _bw.DoWork += BwOnDoWork;
    
         // 綁定工作完成事件
         _bw.WorkerSupportsCancellation = true;
         _bw.RunWorkerCompleted += BwOnRunWorkerCompleted;
    
         // 綁定工作進度更新事件
         _bw.WorkerReportsProgress = true;
         _bw.ProgressChanged += BwOnProgressChanged;
     }
    
     private void BwOnProgressChanged(object sender, ProgressChangedEventArgs e)
     {
         Console.WriteLine($"當前進度:{e.ProgressPercentage}%");
     }
    
     private void BwOnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
     {
         if (e.Cancelled)
         {
             Console.WriteLine("任務已經被取消。");
         }
    
         if (e.Error != null)
         {
             Console.WriteLine("執行任務的過程中出現了異常。");
         }
    
         // 在當前線程可以直接更新 UI 組件的數據
    
         Console.WriteLine($"執行完成的結果:{e.Result}");
     }
    
     public void Run()
     {
         _bw.RunWorkerAsync(10);
     }
    
     private void BwOnDoWork(object sender, DoWorkEventArgs e)
     {
         // 這裡是工作線程進行執行的
    
         Console.WriteLine($"需要計算的數據值為:{e.Argument}");
    
         for (int i = 0; i <= 100; i += 20)
         {
             if (_bw.CancellationPending)
             {
                 e.Cancel = true;
                 return;
             }
    
             _bw.ReportProgress(i);
         }
    
    
         // 傳遞完成的數據給完成事件
         e.Result = 1510;
     }
    }
  9. BackgroundWorker 不是密閉類,用戶可以繼承自 BackgroundWorker 類型,並重寫其 DoWork() 方法以達到自己的需要。

三、線程的中斷與中止

  1. 所有 阻塞 方法在解除阻塞的條件沒有滿足,並且其沒有指定超時時間的情況下,會永久阻塞。

  2. 開發人員可以通過 Thread.Interrupt()Thread.Abort() 方法來解除阻塞。

  3. 在使用線程中斷與中止方法的時候,應該十分謹慎,這可能會導致一些意想不到的情況發生。

  4. 為了演示上面所說的概念,可以編寫如下代碼進行測試。

    class Program
    {
     static void Main()
     {
         var test = new ThreadInterrupt();
         test.Run();
         Console.ReadLine();
     }
    }
    
    public class ThreadInterrupt
    {
     public void Run()
     {
         var testThread = new Thread(WorkThread);
    
         testThread.Start();
            // 中斷指定的線程
         testThread.Interrupt();
     }
    
     private void WorkThread()
     {
         try
         {
             // 永遠阻塞
             Thread.Sleep(Timeout.Infinite);
         }
         catch (ThreadInterruptedException e)
         {
             Console.WriteLine("產生了中斷異常.");
         }
    
         Console.WriteLine("線程執行完成.");
     }
    }

3.1 中斷

  1. 在一個阻塞線程上調用 Thread.Interrupt() 方法,會導致該線程拋出 ThreadInterruptedException 異常,並且強制釋放線程。
  2. 中斷線程時,除非沒有對 ThreadInterruptedException 進行處理,否則是不會導致阻塞線程結束的。
  3. 隨意中斷一個線程是十分危險的,我們可以通過信號構造或者取消構造。哪怕是使用 Thread.Abort() 來中止線程,都比中斷線程更加安全。
  4. 因為隨意中斷線程會導致調用棧上面的任何框架,或者第三方的方法意外接收到中斷。

3.2 中止

Thread.Abort() 方法在 .NET Core 當中無法使用,調用該方法會拋出 Thread abort is not supported on this platform. 錯誤。

  1. 在一個阻塞線程上調用 Thread.Abort() 方法,效果與中斷相似,但會拋出一個 ThreadAbortException 異常。
  2. 該異常在 catch 塊結束之後會被重新拋出。
  3. 未經處理的 ThreadAbortException 是僅有的兩個不會導致應用程式關閉的異常之一。
  4. 中止與中斷最大的不同是,中止操作會立即在執行的地方拋出異常。例如中止發生在 FileStream 的構造期間,可能會導致一個非托管文件句柄保持打開狀態導致記憶體泄漏。

四、安全取消

  1. 與實現了 EAP 模式的 BackgroundWorker 類型一樣,我們可以通過協作模式,使用一個標識來優雅地中止線程。

  2. 其核心思路就是封裝一個取消標記,將其傳入到線程當中,線上程執行時可以通過這個取消標記來優雅中止。

    class Program
    {
     static void Main()
     {
         var test = new CancelTest();
         test.Run();
         Console.ReadLine();
     }
    }
    
    public class CancelToken
    {
     private readonly object _selfLocker = new object();
     private bool _cancelRequest = false;
    
     /// <summary>
     /// 當前操作是否已經被取消。
     /// </summary>
     public bool IsCancellationRequested
     {
         get
         {
             lock (_selfLocker)
             {
                 return _cancelRequest;
             }
         }
     }
    
     /// <summary>
     /// 取消操作。
     /// </summary>
     public void Cancel()
     {
         lock (_selfLocker)
         {
             _cancelRequest = true;
         }
     }
    
     /// <summary>
     /// 如果操作已經被取消,則拋出異常。
     /// </summary>
     public void ThrowIfCancellationRequested()
     {
         lock (_selfLocker)
         {
             if (_cancelRequest)
             {
                 throw new OperationCanceledException("操作被取消.");
             }
         }
     }
    }
    
    public class CancelTest
    {
     public void Run()
     {
         var cancelToken = new CancelToken();
    
         var workThread = new Thread(() =>
         {
             try
             {
                 Work(cancelToken);
             }
             catch (OperationCanceledException e)
             {
                 Console.WriteLine("任務已經被取消。");
             }
         });
    
         workThread.Start();
    
         Thread.Sleep(1000);
         cancelToken.Cancel();
     }
    
     private void Work(CancelToken token)
     {
         // 模擬耗時操作
         while (true)
         {
             token.ThrowIfCancellationRequested();
             try
             {
                 RealWork(token);
             }
             finally
             {
                 // 清理資源
             }
         }
     }
    
     private void RealWork(CancelToken token)
     {
         token.ThrowIfCancellationRequested();
         Console.WriteLine("我是真的在工作...");
     }
    }

4.1 取消標記

  1. 在 .NET 提供了 CancellationTokenSourceCancellationToken 來簡化取消操作。

  2. 如果需要使用這兩個類,則只需要實例化一個 CancellationTokenSource 對象,並將其 Token 屬性傳遞給支持取消的方法,在需要取消的使用調用 Source 的 Cancel() 即可。

    // 偽代碼
    var cancelSource = new CancellationTokenSource();
    
    // 啟動線程
    new Thread(() => work(cancelSource.Token)).Start();
    
    // Work 方法的定義
    void Work(CancellationToken cancelToken)
    {
        cancelToken.ThrowIfCancellationRequested();
    }
    
    // 需要取消的時候,調用 Cancel 方法。
    cancelSource.Cancel();

五、延遲初始化

  1. 延遲初始化的作用是緩解類型構造的開銷,尤其是某個類型的構造開銷很大的時候可以按需進行構造。

    // 原始代碼
    public class Foo
    {
        public readonly Expensive Expensive = new Expensive();
    }
    
    public class Expensive
    {
        public Expensive()
        {
            // ... 構造開銷極大
        }
    }
    
    // 按需構造
    public class LazyFoo
    {
        private Expensive _expensive;
    
        public Expensive Expensive
        {
            get
            {
                if(_expensive == null) _expensive = new Expensive();
            }
        }
    }
    
    // 按需構造的線程安全版本
    public class SafeLazyFoo
    {
        private Expensive _expensive;
        private readonly object _lazyLocker = new object();
    
        public Expensive Expensive
        {
            get
            {
                lock(_lazyLocker)
                {
                    if(_expensive == null)
                    {
                        _expensive = new Expensive();
                    }
                }
            }
        }
    }
  2. 在 .NET 4.0 之後提供了一個 Lazy<T> 類型,可以免去上面複雜的代碼編寫,並且也實現了雙重鎖定模式。

  3. 通過在創建 Lazy<T> 實例時傳遞不同的 bool 參數來決定是否創建線程安全的初始化模式,傳遞了 true 則是線程安全的,傳遞了 false 則不是線程安全的。

    public class LazyExpensive
    {
    
    }
    
    public class LazyTest
    {
        // 線程安全版本的延遲初始化對象。
        private Lazy<LazyExpensive> _lazyExpensive = new Lazy<LazyExpensive>(()=>new LazyExpensive(),true);
    
        public LazyExpensive LazyExpensive => _lazyExpensive.Value;
    }

5.1 LazyInitializer

  1. LazyInitializer 是一個靜態類,基本與 Lazy<T> 相似,但是提供了一系列的靜態方法,在某些極端情況下可以改善性能。

    public class LazyFactoryTest
    {
     private LazyExpensive _lazyExpensive;
    
     // 雙重鎖定模式。
     public LazyExpensive LazyExpensive
     {
         get
         {
             LazyInitializer.EnsureInitialized(ref _lazyExpensive, () => new LazyExpensive());
             return _lazyExpensive;
         }
     }
    
    }
  2. LazyInitializer 提供了一個競爭初始化的版本,這種在多核處理器(線程數與核心數相等)的情況下速度比雙重鎖定技術要快。

    volatile Expensive _expensive;
    public Expensive Expensive
    {
      get
      {
        if (_expensive == null)
        {
          var instance = new Expensive();
          Interlocked.CompareExchange (ref _expensive, instance, null);
        }
        return _expensive;
      }
    }

六、線程局部存儲

  1. 某些數據不適合作為全局遍歷和局部變數,但是在整個調用棧當中又需要進行共用,是與執行路徑緊密相關的。所以這裡來說,應該是在代碼的執行路徑當中是全局的,這裡就可以通過線程來達到數據隔離的效果。例如線程 A 調用鏈是這樣的 A() -> B() -> C()。

  2. 對靜態欄位增加 [ThreadStatic] ,這樣每個線程就會擁有獨立的副本,但僅適用於靜態欄位。

    [ThreadStatic] static int _x;
  3. .NET 提供了一個 ThreadLocal<T> 類型可以用於靜態欄位和實例欄位的線程局部存儲。

    // 靜態欄位存儲
    static ThreadLocal<int> _x = new ThreadLocal<int>(() => 3);
    
    // 實例欄位存儲
    var localRandom = new ThreadLocal<Random>(() => new Random());
  4. ThreadLocal<T> 的值是 延遲初始化 的,第一次被使用的時候 才通過工廠進行初始化。

  5. 我們可以使用 Thread 提供的 Thread.GetData()Thread.SetData() 方法來將數據存儲線上程數據槽當中。

  6. 同一個數據槽可以跨線程使用,而且它在不同的線程當中數據仍然是獨立的。

  7. 通過 LocalDataStoreSolt 可以構建一個數據槽,通過 Thread.GetNamedDataSlot("securityLevel") 來獲得一個命名槽,可以通過 Thread.FreeNameDataSlot("securityLevel") 來釋放。

  8. 如果不需要命名槽,也可以通過 Thread.AllocateDataSlot() 來獲得一個匿名槽。

    class Program
    {
     static void Main()
     {
         var test = new ThreadSlotTest();
         test.Run();
         Console.ReadLine();
     }
    }
    
    public class ThreadSlotTest
    {
     // 創建一個命名槽。
     private LocalDataStoreSlot _localDataStoreSlot = Thread.GetNamedDataSlot("命名槽");
     // 創建一個匿名槽。
     private LocalDataStoreSlot _anonymousDataStoreSlot = Thread.AllocateDataSlot();
    
     public void Run()
     {
         new Thread(NamedThreadWork).Start();
         new Thread(NamedThreadWork).Start();
    
         new Thread(AnonymousThreadWork).Start();
         new Thread(AnonymousThreadWork).Start();
    
         // 釋放命名槽。
         Thread.FreeNamedDataSlot("命名槽");
     }
    
     // 命名槽測試。
     private void NamedThreadWork()
     {
         // 設置命名槽數據
         Thread.SetData(_localDataStoreSlot,DateTime.UtcNow.Ticks);
    
         var data = Thread.GetData(_localDataStoreSlot);
         Console.WriteLine($"命名槽數據:{data}");
    
         ContinueNamedThreadWork();
     }
    
     private void ContinueNamedThreadWork()
     {
         Console.WriteLine($"延續方法中命名槽的數據:{Thread.GetData(_localDataStoreSlot)}");
     }
    
     // 匿名槽測試。
     private void AnonymousThreadWork()
     {
         // 設置匿名槽數據
         Thread.SetData(_anonymousDataStoreSlot,DateTime.UtcNow.Ticks);
    
         var data = Thread.GetData(_anonymousDataStoreSlot);
         Console.WriteLine($"匿名槽數據:{data}");
    
         ContinueAnonymousThreadWork();
     }
    
     private void ContinueAnonymousThreadWork()
     {
         Console.WriteLine($"延續方法中匿名槽的數據:{Thread.GetData(_anonymousDataStoreSlot)}");
     }
    }

七、定時器

7.1 多線程定時器

  1. 多線程定時器使用線程池觸發時間,也就意味著 Elapsed 事件可能會在不同線程當中觸發。
  2. System.Threading.Timer 是最簡單的多線程定時器,而 System.Timers.Timer 則是對於該計時器的封裝。
  3. 多線程定時器的精度大概在 10 ~ 20 ms。

7.2 單線程定時器

  1. 單線程定時器依賴於 UI 模型的底層消息迴圈機制,所以其 Tick 事件總是在創建該定時器的線程觸發。
  2. 單線程定時器關聯的事件可以安全地操作 UI 組件。
  3. 精度比多線程定時器更低,而且更容易使 UI 失去響應。

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

-Advertisement-
Play Games
更多相關文章
  • 春節假期結束了,大家陸續地重回到原來的生活軌道上。假期是一個很好的休息與調節的機會,同時,春節還有辭舊迎新的本意,它是新的輪迴的開端。 在 Python 社區里,剛發生了一件大事,同樣有開啟新紀元的意義:在"Python 之父" Guido van Rossum 宣佈卸任 BDFL(終身仁慈獨裁者) ...
  • 本著做題的心態,上了東莞理工學院的 oj 網;看了一下題目不想動手,在選擇難度的時候發現有些通過率和難度可能存在著某些關係,於是決定爬下這些數據簡單查看一下是否存在關係。 一、新建項目 我是用 Scrapy 框架爬取的(因為剛學沒多久,順便練練手)。首先,先新建 project (下載 Scarpy ...
  • wrk 是一個很簡單的 http 性能測試工具,沒有Load Runner那麼複雜,他和 apache benchmark(ab)同屬於HTTP性能測試工具,但是比 ab 功能更加強大,並且可以支持lua腳本來創建複雜的測試場景。 wrk 的一個很好的特性就是能用很少的線程壓出很大的併發量,原因是它 ...
  • 最近一位5年開發經驗的群友與我聊天 他說:最近慢慢的嘗試去看spring的源碼,學習spring,以前都只是會用就行了,但是越是到後面,發現只懂怎麼用還不夠,在面試的時候經常被問到一些開源框架的源碼問題,即使在網上各種百度,當時回答出來也會是很皮毛,不痛不癢的解答。 對於目前碰到的瓶頸,他不知道怎麼 ...
  • tomcat啟動maven工程的時候提示如下錯誤信息: An internal error occurred during: "Updating status for Tomcat v7.0 Server at localhost..." java.lang.NullPointerException ...
  • 1. server_code01 2. server_code02 3. server_code03 ...
  • 神奇的事件,折磨我 電腦關機重啟了一下關機之前正常的狀態沒有任何的異常出現,過了一會開機準備工作。神奇的事情tmd出現了!!!! 打開phpstudy 啟動... 嗯?apache亮紅報錯?? 第一反應趕快CMD httpd -t 查看咋地了 剛纔還好好的呢,咋一下掛了。 提示信息: httpd: ...
  • 一.模型配置概述 EF使用一組約定基於實體類的定義來構建模型。 可指定其他配置以補充或替代約定的內容。本系列介紹的配置可應用於面向任何數據存儲的模型,以及面向任意關係資料庫時可應用的配置。 資料庫提供程式還可支持特定於具體數據存儲的配置,如Microsoft.EntityFrameworkCore. ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...