.Net 中各種線程同步鎖

来源:https://www.cnblogs.com/newton/p/18365359
-Advertisement-
Play Games

編程編的久了,總會遇到多線程的情況,有些時候我們要幾個線程合作完成某些功能,這時候可以定義一個全局對象,各個線程根據這個對象的狀態來協同工作,這就是基本的線程同步。 支持多線程編程的語言一般都內置了一些類型和方法用於創建上述所說的全局對象也就是鎖對象,它們的作用類似,使用場景有所不同。.Net中這玩 ...


編程編的久了,總會遇到多線程的情況,有些時候我們要幾個線程合作完成某些功能,這時候可以定義一個全局對象,各個線程根據這個對象的狀態來協同工作,這就是基本的線程同步

支持多線程編程的語言一般都內置了一些類型和方法用於創建上述所說的全局對象也就是鎖對象,它們的作用類似,使用場景有所不同。.Net中這玩意兒有很多,若不是經常使用,我想沒人能完全記住它們各自的用法和相互的區別。為了便於查閱,現將它們記錄在此。

ps:本文雖然關註 .Net 平臺,但涉及到的大部分鎖概念都是平臺無關的,在很多其它語言(如_Java__)中都能找到對應。_


鎖模式

正式介紹各種鎖之前,先瞭解下鎖模式——鎖分為內核模式鎖用戶模式鎖,後面也有了混合模式鎖

內核模式就是在系統級別讓線程中斷,收到信號時再切回來繼續幹活。該模式線上程掛起時由系統底層負責,幾乎不占用 CPU 資源,但線程切換時效率低。

用戶模式就是通過一些 CPU 指令或者死迴圈讓線程一直運行著直到可用。該模式下,線程掛起會一直占用 CPU 資源,但線程切換非常快。

長時間的鎖定,優先使用內核模式鎖;如果有大量的鎖定,且鎖定時間非常短,切換頻繁,用戶模式鎖就很有用。另外內核模式鎖可以實現跨進程同步,而用戶模式鎖只能進程內同步

本文中,除後半部分輕量級同步原語自定義鎖為用戶模式鎖,其它鎖都為內核模式。

lock 關鍵字

lock 應該是大多數開發人員最常用的鎖操作,此處不贅述。需要註意的是使用時應 lock 範圍儘量小,lock 時間儘量短,避免無謂等待。

Monitor

上面 lock 就是Monitor的語法糖,通過編譯器編譯會生成 Monitor 的代碼,如下:

lock (syscRoot)
{
    //synchronized region
}
//上面的lock鎖等同於下麵Monitor
Monitor.Enter(syscRoot);
try
{
    //synchronized region
}
finally
{
    Monitor.Exit(syscRoot);
}

Monitor 還可以設置超時時間,避免無限制的等待。同時它還有 Pulse\PulseAll\Wait 實現喚醒機制。

ReaderWriterLock

很多時候,對資源的讀操作頻率要遠遠高於寫操作頻率,這種情況下,應該對讀寫應用不同的鎖,使得在沒有寫鎖時,可以併發讀(加讀鎖),在沒有讀鎖或寫鎖時,才可以寫(加寫鎖)。ReaderWriterLock就實現了此功能。

主要的特點是在沒有寫鎖時,可以併發讀,而非一概而論,不論讀寫都只能一次一個線程。

MethodImpl(MethodImplOptions.Synchronized)

如果是方法層面的線程同步,除上述的lock/Monitor之外,還可以使用MethodImpl(MethodImplOptions.Synchronized)特性修飾目標方法。

SynchronizationAttribute

ContextBoundObject

要瞭解SynchronizationAttribute,不得不先說說ContextBoundObject

首先進程中承載程式集運行的邏輯分區我們稱之為AppDomain(應用程式域),在應用程式域中,存在一個或多個存儲對象的區域我們稱之為Context(上下文)

在上下文的介面當中存在著一個消息接收器負責檢測攔截和處理信息。當對象是MarshalByRefObject的子類的時候,CLR將會建立Transparent Proxy,實現對象與消息之間的轉換。應用程式域是 CLR 中資源的邊界。一般情況下,應用程式域中的對象不能被外界的對象所訪問,而MarshalByRefObject 的功能就是允許在支持遠程處理的應用程式中跨應用程式域邊界訪問對象,在使用.NET Remoting遠程對象開發時經常使用到的一個父類。

ContextBoundObject更進一步,它繼承 MarshalByRefObject,即使處在同一個應用程式域內,如果兩個 ContextBoundObject 所處的上下文不同,在訪問對方的方法時,也會藉由Transparent Proxy實現,即採用基於消息的方法調用方式。這使得 ContextBoundObject 的邏輯永遠在其所屬的上下文中執行。

ps: 相對的,沒有繼承自 ContextBoundObjec t的類的實例則被視為上下文靈活的(context-agile),可存在於任意的上下文當中。上下文靈活的對象總是在調用方的上下文中執行。


一個進程內可以包括多個應用程式域,也可以有多個線程。線程可以穿梭於多個應用程式域當中,但在同一個時刻,線程只會處於一個應用程式域內。線程也能穿梭於多個上下文當中,進行對象的調用。

SynchronizationAttribute用於修飾ContextBoundObject,使得其內部構成一個同步域,同一時段內只允許一個線程進入。


WaitHandle

在查閱一些非同步框架的源碼或介面時,經常能看到WaitHandle這個東西。WaitHandle 是一個抽象類,它有個核心方法WaitOne(int millisecondsTimeout, bool exitContext),第二個參數表示在等待前退出同步域。在大部分情況下這個參數是沒有用的,只有在使用SynchronizationAttribute修飾ContextBoundObject進行同步的時候才有用。它使得當前線程暫時退出同步域,以便其它線程進入。具體請看上述 SynchronizationAttribute 小節。

WaitHandle 包含有以下幾個派生類:

  1. ManualResetEvent
  2. AutoResetEvent
  3. CountdownEvent
  4. Mutex
  5. Semaphore

ManualResetEvent

可以阻塞一個或多個線程,直到收到一個信號告訴 ManualResetEvent 不要再阻塞當前的線程。 註意所有等待的線程都會被喚醒。

可以想象 ManualResetEvent 這個對象內部有一個信號狀態來控制是否要阻塞當前線程,有信號不阻塞,無信號則阻塞。這個信號我們在初始化的時候可以設置它,如ManualResetEvent event=new ManualResetEvent(false);這就表明預設的屬性是要阻塞當前線程。

代碼舉例:

ManualResetEvent _manualResetEvent = new ManualResetEvent(false);

private void ThreadMainDo(object sender, RoutedEventArgs e)
{
    Thread t1 = new Thread(this.Thread1Foo);
    t1.Start(); //啟動線程1
    Thread t2 = new Thread(this.Thread2Foo);
    t2.Start(); //啟動線程2
    Thread.Sleep(3000); //睡眠當前主線程,即調用ThreadMainDo的線程
    _manualResetEvent.Set();   //有信號
}

void Thread1Foo()
{
    //阻塞線程1
    _manualResetEvent.WaitOne();
    
    MessageBox.Show("t1 end");
}

void Thread2Foo()
{
    //阻塞線程2
    _manualResetEvent.WaitOne();
    
    MessageBox.Show("t2 end");
}

AutoResetEvent

用法上和 ManualResetEvent 差不多,不再贅述,區別在於內在邏輯。

與 ManualResetEvent 不同的是,當某個線程調用Set方法時,只有一個等待的線程會被喚醒,並被允許繼續執行。如果有多個線程等待,那麼只會隨機喚醒其中一個,其它線程仍然處於等待狀態。

另一個不同點,也是為什麼取名Auto的原因:AutoResetEvent.WaitOne()會自動將信號狀態設置為無信號。而一旦ManualResetEvent.Set()觸發信號,那麼任意線程再調用 ManualResetEvent.WaitOne() 就不會阻塞,除非在此之前先調用anualResetEvent.Reset()重置為無信號。

CountdownEvent

它的信號有計數狀態,可遞增AddCount()或遞減Signal(),當到達指定值時,將會解除對其等待線程的鎖定。

註意:CountdownEvent 是用戶模式鎖。

Mutex

Mutex 這個對象比較“專制”,同時段內只能准許一個線程工作。

Semaphore

對比 Mutex 同時只有一個線程工作,Semaphore 可指定同時訪問某一資源或資源池的最大線程數。


輕量級同步

.NET Framework 4 開始,System.Threading 命名空間中提供了六個新的數據結構,這些數據結構允許細粒度的併發和並行化,並且降低一定必要的開銷,它們稱為輕量級同步原語,它們都是用戶模式鎖,包括:

  • Barrier
  • CountdownEvent(上文已介紹)
  • ManualResetEventSlim (ManualResetEvent 的輕量替代,註意,它並不繼承 WaitHandle)
  • SemaphoreSlim (Semaphore 輕量替代)
  • SpinLock (可以認為是 Monitor 的輕量替代)
  • SpinWait

Barrier

當在需要一組任務並行地運行一連串的階段,但是每一個階段都要等待其他任務完成前一階段之後才能開始時,您可以通過使用Barrier類的實例來同步這一類協同工作。當然,我們現在也可以使用非同步Task方式更直觀地完成此類工作。

SpinWait

如果等待某個條件滿足需要的時間很短,而且不希望發生昂貴的上下文切換,那麼基於自旋的等待時一種很好的替換方案。SpinWait不僅提供了基本自旋功能,而且還提供了SpinWait.SpinUntil方法,使用這個方法能夠自旋直到滿足某個條件為止。此外 SpinWait 是一個Struct,從記憶體的角度上說,開銷很小。

需要註意的是:長時間的自旋不是很好的做法,因為自旋會阻塞更高級的線程及其相關的任務,還會阻塞垃圾回收機制。SpinWait 並沒有設計為讓多個任務或線程併發使用,因此需要的話,每一個任務或線程都應該使用自己的 SpinWait 實例。

當一個線程自旋時,會將一個內核放入到一個繁忙的迴圈中,而不會讓出當前處理器時間片剩餘部分,當一個任務或者線程調用Thread.Sleep方法時,底層線程可能會讓出當前處理器時間片的剩餘部分,這是一個大開銷的操作。

因此,在大部分情況下, 不要在迴圈內調用 Thread.Sleep 方法等待特定的條件滿足。可以認為 SpinWait 是 Thread.Sleep 的輕量替換。它並非鎖,但可以通過它實現自定義鎖(下文會講到)。

SpinLock是對 SpinWait 的簡單封裝。


自定義鎖

由 SpinWait 使用方法易知,搭配一個或多個全局條件,就可以實現自定義鎖。除此之外,還有一些東東,本身並不屬於鎖的範疇,但可藉助以實現自定義鎖,比如下麵描述的volatileInterlocked

volatile 關鍵字

volatile最初是為瞭解決緩存一致性問題引入的。

緩存一致性

瞭解緩存一致性,首先要瞭解.Net/Java的記憶體模型(.Net 當年是諸多借鑒了 Java 的設計理念)。而 Java 記憶體模型又借鑒了硬體層面的設計。

我們知道,在現代電腦中,處理器的指令速度遠超記憶體的存取速度,所以現代電腦系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的高速緩存來作為主存與處理器之間的緩衝。處理器計算直接存取的是高速緩存中的數據,計算完畢後再同步到主存中。

在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共用同一主存。

而 Java 記憶體模型的每個線程有自己的工作記憶體,其中保留了被線程使用的變數的副本。線程對變數的所有的操作都必須在工作記憶體中完成,而不能直接讀寫主記憶體中的變數。不同線程之間也不能直接訪問對方工作記憶體中的變數,線程間變數的值的傳遞需要通過主記憶體中轉來完成。

雖然兩者的設計相似,但是前者主要解決存取效率不匹配的問題,而後者主要解決記憶體安全(競爭、泄露)方面的問題。顯而易見,這種設計方案引入了新的問題——緩存一致性(CacheCoherence)——即各工作記憶體、工作記憶體與主存,它們存儲的相同變數對應的值在同一時刻可能不一樣。


為瞭解決這個問題,很多平臺都內置了 volatile 關鍵字,使用它修飾的變數,可以保證所有線程每次獲取到的是最新值。這是怎麼做到的呢?這就要求所有線程在訪問變數時遵循預定的協議,比如MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等,此處不贅述,只需要知道系統額外幫我們做了一些事情,多少會影響執行效率。

另外 volatile 還能避免編譯器自作聰明重排指令。重排指令在大多數時候無傷大雅,還能對執行效率有一定提升,但某些時候會影響到執行結果,此時就可以使用 volatile。

Interlocked

同 volatile 的可見性作用類似,Interlocked 可為多個線程共用的變數提供原子操作,這個類是一個靜態類,它提供了以線程安全的方式遞增、遞減、交換和讀取值的方法。

它的原子操作基於 CPU 本身,非阻塞,所以也不是真正意義上的鎖,當然效率會比鎖高得多。

原子操作

電腦中的原子操作有兩層含義:

  • 在執行過程中不會被中斷或干擾的操作,是不可分割的操作單元,要麼全部執行成功,要麼全部不執行;
  • 多線程/進程對“同時”進行同一個原子操作,不會相互產生干擾導致預期之外的結果。這在單核和多核情況下又有不同考量——在單核 CPU 中,原子操作通常是指在一個指令周期內可以完成的操作,不會被中斷,例如賦值、遞增、遞減等操作;在多核 CPU 中,原子操作需要考慮多個核心同時訪問共用資源的情況,需要使用特殊的機制來確保操作的原子性,如硬體支持的原子指令或鎖機制。

這兩個特點,使得原子操作可作為內部條件實現自定義鎖。


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

-Advertisement-
Play Games
更多相關文章
  • 1. Rust 簡介 Rust 的歷史 起源:Rust 語言最初由 Mozilla 研究員 Graydon Hoare 於 2006 年開始設計,並於 2009 年首次公開。 開發:Rust 是 Mozilla 實驗室的一個項目,目的是創建一種能夠保證記憶體安全同時又不犧牲性能的系統編程語言。 發佈: ...
  • 1. make 和 Makefile 1.1. 什麼是make? 1.2. 什麼是Makefile? 1.3. make 與 Makefile的關係 2. Makefile的語法 2.1. 基本語法 2.2. 變數 2.3. 偽目標 2.4. 模式規則 2.5. 自動變數 2.6. 條件判斷 3. ...
  • Torna —— 一款介面文檔解決方案,目標是讓介面文檔管理變得更加方便、快捷。採用團隊協作的方式管理和維護介面文檔,將不同形式的文檔納入進來統一維護。 ...
  • 大多數的業務場景下 PHP 還沒有達到性能瓶頸,然而 MySQL 資料庫就先行駕崩了。但我們總是不分青紅皂白,一股腦的把原因歸結於是 PHP 語言不行了,每當遇到這種情形我就會感嘆到 PHP 的命真苦啊。 ...
  • 大家好,我是曉凡。 寫在前面 不知道大家有沒有做財務的朋友,我就有這麼一位朋友就經常跟我抱怨。一到月底簡直就是噩夢,總有加不完的班,熬不完的夜,做不完的報表。 一聽到這兒,這不就一活生生的一個“大表哥”麽,這加班跟我們程式員有得一拼了,忍不住邪惡一笑,心裡平衡了很多。 身為牛馬,大家都不容易啊。我不 ...
  • 目錄Razor 類庫創建使用使可路由組件可從 RCL 獲取靜態資源表單EditForm標準輸入組件驗證HTML 表單 Razor 類庫 這裡只對 RCL 創建和使用的做一些簡單的概述,詳細內容參考官方文檔 使用 Razor 類庫 (RCL) 中的 ASP.NET Core Razor 組件。 創建 ...
  • 泛型(Generics)是C#中的一個重要特性,它允許您編寫靈活、類型安全且可重用的代碼。下麵我將詳細介紹泛型的概念、使用方法及其在C#中的實現細節。 泛型的基本概念 1. 什麼是泛型? 泛型是一種允許您定義類型參數的機制,這些類型參數可以在編譯時由具體的類型替換。這樣,您可以編寫一個通用的類或方法 ...
  • VS常用拓展以及快捷鍵 擴展1:Select Next Occurrence 該拓展可以當前目標、下一個目標、上一個目標,類似於Alt+滑鼠拖動,但是可以在沒對齊的情況下使用 安裝 設置4個常用的快捷鍵 工具->選項->鍵盤->c# 2005 選擇下一個 快捷鍵:Ctrl+D 選擇上一個 快捷鍵:C ...
一周排行
    -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# ...