【.NET】多線程:自動重置事件與手動重置事件的區別

来源:https://www.cnblogs.com/tcjiaan/archive/2023/11/11/17826114.html
-Advertisement-
Play Games

在多線程編程中,如果每個線程的運行不是完全獨立的。那麼,一個線程執行到某個時刻需要知道其他線程發生了什麼。嗯,這就是所謂線程同步。同步事件對象(XXXEvent)有兩種行為: 1、等待。線程在此時會暫停運行,等待其他線程發出信號才繼續(等你約); 2、發出信號。當前線程發出信號,其他正在等待線程收到 ...


在多線程編程中,如果每個線程的運行不是完全獨立的。那麼,一個線程執行到某個時刻需要知道其他線程發生了什麼。嗯,這就是所謂線程同步。同步事件對象(XXXEvent)有兩種行為:

1、等待。線程在此時會暫停運行,等待其他線程發出信號才繼續(等你約);

2、發出信號。當前線程發出信號,其他正在等待線程收到信號後繼續運行(我約你)。

從前,小明、小偉、小更、小紅、小黃計划到野外去烤魚吃。但他們只確定市郊東南方向的一片區域,並不能保證具體哪個地點適合燒烤。於是,他們商量好,大家同時從家裡出發。小明離那裡比較近,他先去考察一下;其他人到了東南郊後集合,等小明的消息。小明考察完畢,向大家群發消息說明選定的地點是F。最後大家繼續前行,奔向F。

等待事件有好幾個:

1、Mutex:互斥體。一次只能有一個線程獲取到互斥體,其他線程只能等。占用互斥體的線程釋放後,其他線程繼續搶 Mutex。然後只有一個線程能搶到,其他線程繼續等……

2、AutoResetEvent:自動事件,發出信號後立刻重置。

3、ManualResetEvent:手動事件,發出信號後不會立刻重置,得手動重置。

4、CountdownEvent:這個和上面兩個差不多。但它會設定一個計數,線程發出信號時會減少計數。被阻止的線程要等到計數 <= 0 時才獲得信號。

 

本次咱們討論的重點是看看自動重置信號和手動重置信號之間有什麼區別。

 先看看自動重置的。

internal class Program
{

    static AutoResetEvent theEvent = new(false);

    static void Main(string[] args)
    {
        // 啟動三個線程
        ThreadPool.QueueUserWorkItem(DoWorking, "A");
        ThreadPool.QueueUserWorkItem(DoWorking, "B");
        ThreadPool.QueueUserWorkItem(DoWorking, "C");
        // 主線程監聽鍵盤消息
        while(true)
        {
            var keyInfo = Console.ReadKey(true);
            // 看看是不是Y鍵
            if(keyInfo.Key == ConsoleKey.Y)
            {
                // 點亮信號
                theEvent.Set();
            }
            // 輸出一行,方便判斷一個迴圈
            Console.WriteLine("------------------------------");
        }
    }

    static void DoWorking(object? state)
    {
        while(true)
        {
            // 等待主線程的信號
            // 此線程會暫停
            theEvent.WaitOne();
            // 得到信號了,繼續運行
            Console.WriteLine("{0}已收到通知", state);
        }
    }
}

這個例子創建了三個線程,這裡我用的是線程池,把一個WaitCallback委托傳給 QueueUserWorkItem 方法就可以線上程池中運行新線程。上面示例中綁定的方法是 DoWorking。

AutoResetEvent 類的構造函數傳了一個 bool 值,它的作用是設置等待事件的初始狀態:

1、如果為 true,表示事件初始狀態為打開信號,這會使正在等的線程馬上得到信號;

2、如果為 false,表示事件的初始狀態為沒有信號,正在等待的線程繼續等。

按照咱們這個例子的實際情況,我們一開始應該讓事件無狀態,讓後臺的三個線程等待。主線程讀取按鍵信息,如果按的是【Y】鍵,那麼事件調用 Set 方法,打開信號。此時,等得花兒都謝了的三個線程會繼續。我們運行一下,看看能否符合預期。

經測試,我們會發現:每次按【Y】後,三個線程中只有一個獲得信號並繼續,其他兩個還在高速上堵車。 AutoResetEvent 的自動重置就是打開信號後又立馬關閉,每次只讓一個線程收到信號。所以,當咱們按一次【Y】鍵後,主線程發出了信號,又馬上關閉。三個後臺線程相互競爭,隨機獲得機會,結束等待並繼續運行。

 

手動重置事件在打開信號後,信號會持續有效,直到調用 Reset 方法手動關閉信號。手動重置信號能讓多個線程有足夠的時間收到信號。

下麵咱們把上面的示例改為使用 ManualResetEvent 類。

internal class Program
{
    static ManualResetEvent theEvent = new(false);

    static void Main(string[] args)
    {
        // 啟動三個線程
        ThreadPool.QueueUserWorkItem(DoWorking, "A");
        ThreadPool.QueueUserWorkItem(DoWorking, "B");
        ThreadPool.QueueUserWorkItem(DoWorking, "C");
        // 主線程監聽鍵盤消息
        while(true)
        {
            var keyInfo = Console.ReadKey(true);
            // 看看是不是Y鍵
            if(keyInfo.Key == ConsoleKey.Y)
            {
                // 點亮信號
                theEvent.Set();

                // 持續一段時間後關閉信號
                Thread.Sleep(3);
                theEvent.Reset();
            }
            // 輸出一行,方便判斷一個迴圈
            Console.WriteLine("------------------------------");
        }
    }

    static void DoWorking(object? state)
    {
        while(true)
        {
            // 等待主線程的信號
            // 此線程會暫停
            theEvent.WaitOne();
            // 得到信號了,繼續運行
            Console.WriteLine("{0}已收到通知", state);
        }
    }
}

然後運行程式,這一次按下【Y】鍵後,三個線程都能收到信號通知了。

你會發現,有些線程重覆了多次,那是因為 DoWorking 方法裡面是個死迴圈。當信號持續打開期間,三個線程都有機會收到信號,甚至會重覆收到。

上面的東東純屬演示,實際使用的話不會這樣設計。最好的方法是建一個列表對象,主線程接收到的按鍵字元存放到一個列表中,然後,後臺線程不斷地從列表中取出元素來處理。這樣設計程式會更流暢。

internal class Program
{
    #region 欄位區域
    static Queue<char> keyChars = new();
    #endregion

    static void Main(string[] args)
    {
        // 啟動三個線程
        ThreadPool.QueueUserWorkItem(DoSomething, "A");
        ThreadPool.QueueUserWorkItem(DoSomething, "B");
        ThreadPool.QueueUserWorkItem(DoSomething, "C");

        while(true)
        {
            // 讀取鍵盤字元
            ConsoleKeyInfo info = Console.ReadKey(true);
            // 將字元放入隊列
            keyChars.Enqueue(info.KeyChar);
        }
    }

    static void DoSomething(object? state)
    {
        while(true)
        {
            // 鎖定
            Monitor.Enter(keyChars);
            if (keyChars.Count > 0)
            {
                // 取掉一個元素
                char c = keyChars.Dequeue();
                Console.WriteLine($"線程【{state}】獲得字元:{c}");
            }
            // 解鎖
            Monitor.Exit(keyChars);
        }
    }
}

這裡我用泛型隊列 Queue<T> 來存放鍵盤敲入的字元,DoSomething 方法將放入線程池中運行。在從隊列中取出元素並處理時,一定要記得上鎖。我用的是 Monitor 對象的靜態方法來上鎖和解鎖,當然你可以用 lock 語句塊。

lock(keyChars)
{
    ……
}

如果不上鎖,線程間在搶占資源時會導致不一致的狀態。當A線程訪問 keyChars.Count 屬性時得到 1,還是 > 0 的,但在取出最後一個元素前,偏偏B線程動作快把最後一個元素拿走了。當A線程執行到 keyChars.Dequeue() 一句時,keyChars 隊列中已經沒有元素了,會發生錯誤。

主線程在 Enqueue 時並不需要鎖定,因為元素送入隊列只有一個線程在做,沒人跟他搶資源,可以不鎖定。

運行程式後,可以按字母、數字等按鍵來測試。畢竟像【F3】、【Ctrl】等按鍵獲取到的是空白 char。

這樣就順暢很多了。

 


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

-Advertisement-
Play Games
更多相關文章
  • 架構目標 高可用性 整體系統可用性最低99.9%,目標99.99%。全年故障時間整個系統不超過500分鐘,單個系統故障不超過50分鐘。 高可擴展性 系統架構簡單清晰,應用系統間耦合低,容易水平擴展,業務功能增改方便快捷。 低成本 增加服務的重用性,提高開發效率,降低人力成本; 最終一致性 服務設計能 ...
  • 本文介紹Util應用框架如何記錄日誌. 日誌記錄共分4篇,本文是正文,後續還有3篇分別介紹寫入不同日誌接收器的安裝和配置方法. 概述 日誌記錄對於瞭解系統執行情況非常重要. Asp.Net Core 抽象了日誌基礎架構,支持使用日誌提供程式進行擴展,提供控制台日誌等簡單實現. Serilog 是 . ...
  • 四、基本數據類型和計算(三) 1、枚舉變數 1)通過案例體現枚舉類型的作用 ​ 假設要為我們的游戲裝備設置稀有度屬性,應該如何設計 裝備級別 變數名 普通 normal 高級 high 稀有 rare 史詩 epic 傳說 legend 神話 myth 不使用枚舉變數,使用常量方式設置 #inclu ...
  • 使用 ORDER BY 進行排序 使用 ORDER BY 語句按升序或降序對結果進行排序。 ORDER BY 關鍵字預設按升序排序。要按降序排序結果,使用 DESC 關鍵字。 示例按名稱按字母順序排序結果: import mysql.connector mydb = mysql.connector. ...
  • 1. pip命令 查看已安裝的包 pip list 安裝包 pip install package_name 卸載包 # 卸載指定包 pip uninstall package_name # 卸載已安裝的所有第三方Python庫 pip freeze > list.txt pip uninstall ...
  • 危機感 距離上一次找工作面試已經過去快2年了,那時候正值疫情肆虐,雖然還未感受到“寒潮來臨”的苗頭,但最終還是成功通過了幾輪面試,順利簽約。在目前公司待了2年了,在大環境的影響下,沒有加薪、沒有年終(這個真的很傷)、各種項目混亂、技術快停滯不前,年末又要過一年了,又離35進一步了,終危機感又來了,不 ...
  • 寫在前面 就在這周三,無意間我在掘金刷到一篇文章,讓我這個35歲的單身老狗又次相信了愛情,而且相信真的會有那種所謂的緣分和相濡以沫、雙向奔赴的愛情。 我又相信了愛情 文中男主是在掘金相親角成功的找到了另一半,而順利結婚,打動我的應該是女主的真誠吧,或許應該說那應該是我最嚮往的愛情,如下文中描述: 簡 ...
  • 在Go語言中,我們通常會遇到兩種主要的方式來處理和操作字元串:使用fmt.Sprintf函數和string.Builder類型。儘管兩者都可以實現字元串的格式化和連接,但它們在性能和用法上有一些關鍵區別。 1. fmt.Sprintf fmt.Sprintf是一個函數,它根據提供的格式化字元串和參數 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...