【.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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...