C#線程同步(二)鎖

目錄

1.lock

2.Monitor

3.鎖的其它要注意的問題

3.1同步對象的選擇

3.2什么時候該上鎖

3.3鎖和原子性

3.4嵌套鎖

3.5 死鎖

3.6 性能

4.Mutex

5.Semaphore


1.lock

讓我們先看一段代碼:

class ThreadUnsafe
{static int _val1 = 1, _val2 = 1;static void Go(){if (_val2 != 0) Console.WriteLine (_val1 / _val2);_val2 = 0;}
}

? ? ? ? 這段代碼在單線程運行時是安全的,但是多線程運行就會出現問題,即有可能在做除法時,出現_val2=0 的情況,這是由于在執行打印時,另一個線程可能會去修改_val2的值。

我們可以通過加鎖來解決這個問題:

class ThreadSafe
{static readonly object _locker = new object();static int _val1, _val2;static void Go(){lock (_locker){if (_val2 != 0) Console.WriteLine (_val1 / _val2);_val2 = 0;}}
}

關鍵字lock保證任何時候,只有一個線程可以訪問變量_val1 _val2

2.Monitor

? ? ? ? 關鍵字lock的機制是靠Monitor類實現的。lock可以認為是利用try-finally 結構對Monitor.Enter和Monitor.Exit 函數進行封裝。比如上面使用lock關鍵字的代碼用Monitor類實現如下:

Monitor.Enter (_locker);
try
{if (_val2 != 0) Console.WriteLine (_val1 / _val2);_val2 
= 0;
}
finally { Monitor.Exit (_locker); }

(ps:在沒有調用Monitor.Enter函數時,調用Monitor.Exit 會拋出異常)

????????然而這段代碼存在一個微妙的漏洞。試想這樣一種(不太可能的)情況:當Monitor.Enter方法內部拋出異常,或是在調用Monitor.Enter之后、進入try代碼塊之前發生異常(例如線程被強制中止Abort,或是拋出內存耗盡異常OutOfMemoryException)。此時鎖可能被獲取,也可能未被獲取。如果鎖已被獲取,它將永遠不會被釋放——因為我們無法進入try/finally代碼塊,最終導致鎖泄漏。 為避免這種風險,CLR 4.0的設計者為Monitor.Enter添加了以下重載方法:

public static void Enter (object obj, ref bool lockTaken);

當(且僅當)Enter方法拋出異常且未成功獲取鎖時,該方法執行后lockTaken參數值為false。以下是正確的使用模式(這也正是C# 4.0編譯lock語句時生成的代碼邏輯):? ?

bool lockTaken = false;
try
{Monitor
.Enter (_locker, ref lockTaken);// Do your stuff...
}
finally { if (lockTaken) Monitor.Exit (_locker); }

這樣一來,即使在Enter函數出現異常,也不會去執行Monitor.Exit函數了。

Monitor 類還提供了?TryEnter?方法,允許指定超時時間(以毫秒或?TimeSpan?形式)。如果成功獲取鎖,該方法返回?true;如果因超時未能獲取鎖,則返回?falseTryEnter?也可以不帶參數調用,此時它會立即“測試”鎖的狀態,如果無法立即獲取鎖,則立刻超時返回。

與?Enter?方法類似,在 CLR 4.0 中,TryEnter?也提供了接受?lockTaken?參數的重載版本。

這里就不過多介紹了,感興趣的可以去看官方鏈接

3.鎖的其它要注意的問題

3.1同步對象的選擇

????????任何對參與線程可見的對象都可以作為同步對象,但必須遵循一個硬性規則:同步對象必須是引用類型。同步對象通常聲明為 private(這有助于封裝鎖邏輯),并且通常是實例字段或靜態字段。同步對象可以同時作為被保護對象本身,如下例中的 _list 字段所示:

class ThreadSafe
{List 
<string> _list = new List <string>();void Test(){lock (_list){_list
.Add ("Item 1");...

專門用于加鎖的字段(如前例中的?_locker)能夠精確控制鎖的作用范圍和粒度。此外,包含對象本身(this)或其類型(typeof(ClassName))也可作為同步對象使用:

lock (this) { ... }
lock (typeof (Widget)) { ... }    // For protecting access to statics

雖然上面兩個鎖對象都是合理的,卻是不建議的:

使用this作為鎖對象會造成:

  • 外部代碼可能鎖定你的對象實例,導致死鎖。
  • 破壞了面向對象的封裝原則。

使用類類型作為鎖對象則更糟糕:typeof(ClassName) 返回的是類的 Type 對象,該對象在 AppDomain 范圍內是唯一的。所有線程中任何使用 lock(typeof(ClassName)) 的代碼都會競爭同一個鎖,導致: ? ?性能瓶頸:無關代碼因共享同一個鎖而阻塞。 ?? ?死鎖風險:第三方庫或框架若恰好也鎖定了該類型,可能引發不可預料的死鎖

3.2什么時候該上鎖

? ? ? ? 首先,如果你確定你的程序是單線程的,那任何時候都不需要上鎖。否則,上鎖基本原則是:任何對可寫共享字段的訪問都需要加鎖即使是最簡單的單字段賦值操作,也必須考慮同步問題。例如以下類中,無論是Increment還是Assign方法都不是線程安全的:

class ThreadUnsafe
{static int _x;static void Increment() { _x++; }static void Assign()??? { _x = 123; }
}

其線程安全的標準應該為:

class ThreadSafe
{static readonly object _locker = new object();static int _x;static void Increment() { lock (_locker) _x++; }static void Assign()??? { lock (_locker) _x = 123; }
}

3.3鎖和原子性

????????如果一組變量總是在同一個鎖內進行讀寫,那么可以認為這些變量的讀寫操作是原子性的。假設字段 x 和 y 始終在對 locker 對象加鎖的情況下進行讀寫:

lock (locker) { if (x != 0) y /= x; }

那么我們可以說 x 和 y 的訪問是原子性的,因為這段代碼塊不會被其他線程的操作分割或搶占,從而避免 x 或 y 被意外修改而導致結果失效。只要 x 和 y 始終在同一個獨占鎖內訪問,就永遠不會發生除零錯誤。

3.4嵌套鎖

? ? ? ? 一個線程可以反復的對一個對象添加鎖:

lock (locker)lock (locker)lock (locker){// Do something...}

或者改用Monitor類:

Monitor.Enter (locker); Monitor.Enter (locker);? Monitor.Enter (locker); 
// Do something...
Monitor
.Exit (locker); ?Monitor.Exit (locker);?? Monitor.Exit (locker);

在這種情況下,只有當最外層的 lock 語句執行完畢退出時 - 或者執行了對應數量的 Monitor.Exit 語句后 - 對象才會被解鎖。 嵌套鎖在方法內部調用另一個加鎖方法時特別有用:

static readonly object _locker = new object();static void Main()
{lock (_locker){AnotherMethod();// We still have the lock - because locks are reentrant.}
}static void AnotherMethod()
{lock (_locker) { Console.WriteLine ("Another method"); }
}

3.5 死鎖

? ? ? ? 死鎖在多線程編程是比較常見的。下面這個代碼就會觸發死鎖:

object locker1 = new object();
object locker2 = new object();new Thread (() => {lock (locker1){Thread.Sleep (1000);lock (locker2);????? // Deadlock}}).Start();
lock (locker2)
{Thread.Sleep (1000);lock (locker1);????????????????????????? // Deadlock
}

????????在多線程編程中,死鎖是最棘手的難題之一——尤其是當存在大量相互關聯的對象時。究其根本,難點在于你永遠無法確定調用方已經獲取了哪些鎖。 設想這樣一個場景:你可能在類X中無意識地鎖定了私有字段a,卻不知道調用方(或調用方的調用方)已經在類Y中鎖定了字段b。與此同時,另一個線程正以相反的順序執行鎖定——這就形成了死鎖。

????????頗具諷刺意味的是,這種問題反而會因(良好的)面向對象設計模式而加劇,因為這些模式創建的調用鏈直到運行時才能確定。 雖然"按固定順序鎖定對象以避免死鎖"的建議在我們最初的示例中很有幫助,但很難適用于上述場景。

????????更明智的策略是:當持有鎖的情況下調用可能反向引用自身對象的方法時要格外謹慎。同時,需要審慎評估是否真的有必要在調用其他類的方法時保持鎖定(雖然很多時候確實需要——我們稍后會討論——但有時存在其他選擇)。更多地依賴聲明式編程、數據并行、不可變類型以及非阻塞同步結構,可以減少對鎖定的依賴。?

????????這個問題還可以換個角度理解:當持有鎖時調用外部代碼,鎖的封裝性就會在無形中被破壞。這不是CLR或.NET框架的缺陷,而是鎖機制與生俱來的局限性。目前包括軟件事務內存(Software Transactional Memory)在內的多個研究項目正在嘗試解決鎖機制帶來的各種問題。

???????? 另一個典型的死鎖場景發生在WPF應用程序調用Dispatcher.Invoke或Windows Forms應用程序調用Control.Invoke時——如果此時恰好持有鎖,而UI線程正在執行另一個等待同一鎖的方法,就會立即引發死鎖。通常只需改用BeginInvoke而非Invoke即可解決。當然,也可以在調用Invoke前釋放鎖,不過如果鎖是由調用方獲取的,這個方法就不適用了。我們將在"富客戶端應用與線程關聯性"章節詳細解釋Invoke和BeginInvoke的機制。

3.6 性能

????????加鎖操作本身非常高效:在2010年代的計算機上,如果鎖未被爭用,獲取和釋放一個鎖最快僅需20納秒。但當鎖出現爭用時,隨之而來的上下文切換會使開銷激增至微秒級別——如果線程需要重新調度,等待時間可能更長。對于極短時間的鎖定,使用SpinLock類可以避免上下文切換的開銷。 需要注意的是,如果鎖持有時間過長,不僅會降低并發性能,還會顯著增加死鎖風險。鎖的爭用會引發線程阻塞,當多個線程相互等待對方釋放鎖時,系統吞吐量將急劇下降。因此,開發者需要在保證線程安全的前提下,盡量縮小臨界區范圍,并考慮使用讀寫鎖(ReaderWriterLockSlim)等更細粒度的同步機制來提升并發性。對于高并發場景,無鎖編程(lock-free programming)或不可變數據結構往往是更好的選擇。

4.Mutex

????????互斥鎖(Mutex)類似于 C# 的 lock 語句,但它的作用范圍可以跨越多個進程。也就是說,Mutex 既可以是應用程序級別的,也可以是計算機全局范圍的。( 獲取和釋放一個無競爭的 Mutex 需要幾微秒時間——這比 lock 語句慢了約 50 倍。)

????????使用 Mutex 類時,你需要調用 WaitOne 方法來加鎖,調用 ReleaseMutex 方法來解鎖。關閉或釋放 Mutex 會自動解除鎖定。與 lock 語句一樣,Mutex 只能由獲取它的同一個線程來釋放。 跨進程 Mutex 的一個常見用途是確保同一時間只能運行一個程序實例。具體實現如下:

class OneAtATimePlease
{static void Main(){// Naming a Mutex makes it available computer-wide. Use a name that's// unique to your company and application (e.g., include your URL).using (var mutex = new Mutex (false, "oreilly.com OneAtATimeDemo")){// Wait a few seconds if contended, in case another instance// of the program is still in the process of shutting down.if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false)){Console.WriteLine ("Another app instance is running. Bye!");return;}RunProgram();}}static void RunProgram(){Console.WriteLine ("Running. Press Enter to exit");Console.ReadLine();}
}

當然也可以這樣實現:

static void Main()
{using var mutex = new Mutex(true, "Global\\MyApp", out bool createdNew);if (!createdNew){Console.WriteLine("程序已在運行中!");return;}// 主程序邏輯Console.WriteLine("程序啟動...");Console.ReadLine();
}

一般而言,mutex在多線程編程中使用的不多,lock是更常見的選擇。但涉及到跨進程時,lock可能就無能為力了,這是可以考慮mutex.

5.Semaphore

????????信號量(Semaphore)就像一家夜總會:它有一定的容量限制,由門口的保安嚴格執行。一旦滿員,其他人就無法進入,只能在門外排隊等候。每當有一個人離開,隊首的一個人就能進入。它的構造函數至少需要兩個參數:當前夜總會內的空位數,以及夜總會的總容量

容量為1的信號量與互斥鎖(Mutex)或lock類似,但關鍵區別在于信號量沒有"所有者"——它對線程是透明的。任何線程都可以調用信號量的Release方法,而Mutex和lock只能由獲取鎖的線程來釋放

? ? ? ? (這個類有兩個功能相似的版本:Semaphore和SemaphoreSlim。后者是在.NET Framework 4.0中引入的,針對并行編程的低延遲需求進行了優化。它在傳統多線程編程中也很有用,因為它允許在等待時指定取消令牌。不過,它不能用于進程間通信。 Semaphore執行WaitOne或Release大約需要1微秒;而SemaphoreSlim只需要前者的四分之一時間。 )

信號量在限制并發度方面非常有用——可以防止過多線程同時執行某段代碼。在下面的例子中,五個線程試圖進入一家同時只允許三個線程進入的"夜總會":

????????

class TheClub      // No door lists!
{static SemaphoreSlim _sem = new SemaphoreSlim (3);    // Capacity of 3static void Main(){for (int i = 1; i <= 5; i++) new Thread (Enter).Start (i);}static void Enter (object id){Console.WriteLine (id + " wants to enter");_sem.Wait();Console.WriteLine (id + " is in!");           // Only three threadsThread.Sleep (1000 * (int) id);               // can be here atConsole.WriteLine (id + " is leaving");       // a time._sem.Release();}
}

執行結果如下:

1 wants to enter
1 is in!
2 wants to enter
2 is in!
3 wants to enter
3 is in!
4 wants to enter
5 wants to enter
1 is leaving
4 is in!
2 is leaving
5 is in!

如果將 Sleep 語句替換為密集的磁盤 I/O 操作,信號量(Semaphore)通過限制過多的并發硬盤訪問,反而能夠提升整體性能。 如果給信號量命名,它就能像互斥鎖(Mutex)一樣實現跨進程同步


本小節就介紹到這里,下面一節將介紹線程安全的一些實現準則。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/917264.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/917264.shtml
英文地址,請注明出處:http://en.pswp.cn/news/917264.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

鴻蒙智能居家養老系統構思(續二)—— 適老化烹飪中心詳細構思

一、背景在“寫給華為鴻蒙智家 —— 智能居家養老系統構思”一文中&#xff0c;結合對居家養老的理解及個人體驗&#xff0c;提出了基于鴻蒙OS實現居家養老系統的粗略構思。其中包含“吃得好”。當老人到了不能隨性外出活動、只能在家消耗時光時&#xff0c;除了一些看看電視、…

高斯透鏡公式(調整鏡頭與感光元件之間的距離時,使得不同距離的物體在感光元件上形成清晰的影像)

當使用定焦鏡頭時&#xff0c;仍然可以調整鏡頭與感光元件&#xff08;或膠片&#xff09;之間的距離時&#xff0c;使得不同距離的物體在感光元件上形成清晰的影像。對此可以用高斯透鏡公式進行解釋&#xff1a; 一、透鏡成像的基本原理 在光學中&#xff0c;一個基本的公式是…

預過濾環境光貼圖制作教程:第三階段 - GGX 分布預過濾

核心目標 GGX 分布是 PBR 中模擬粗糙表面高光反射的主流模型,其核心是通過統計分布描述微表面的朝向概率。本階段的目標是: 基于第一階段生成的環境圖集,預計算 6 個級別的 GGX 過濾結果(對應不同粗糙度); 使用蒙特卡洛采樣(Monte Carlo Sampling)加速 GGX 卷積計算;…

Spring框架與AutoCAD結合應用

什么是AutoCAD? AutoCAD簡介 AutoCAD是由美國Autodesk公司開發的計算機輔助設計(CAD)軟件,廣泛應用于建筑、工程、制造、產品設計等領域。它支持2D繪圖和3D建模,提供精確的圖形工具和自動化功能,幫助用戶高效創建技術圖紙和模型。 主要功能 2D繪圖:提供直線、圓弧、多…

Java 學習筆記:常用類、String 與日期時間處理

作為一名名 Java 初學者&#xff0c;最近在學習過程中整理了一些關于常用類、String 類以及日期時間處理的知識點。這些內容是 Java 基礎中的重點&#xff0c;也是日常編程練習中頻繁用到的工具&#xff0c;掌握它們能讓我們在寫代碼時更加得心應手。今天把這些筆記分享出來&am…

Android常用的adb和logcat命令

ADB ADB&#xff0c;即 Android Debug Bridge 是一種允許模擬器或已連接的 Android 設備進行通信的命令行工具&#xff0c;它可為各種設備操作提供便利&#xff0c;如安裝和調試應用&#xff0c;并提供對 Unix shell&#xff08;可用來在模擬器或連接的設備上運行各種命令&…

重學JS-001 --- JavaScript算法與數據結構(一)JavaScript 基礎知識

文章目錄 變量 變量命名規則 變量命名 let vs const 變量使用范圍 賦值 = 控制臺輸出 運算符 ++ -- == === !== 注釋 轉義字符 數據類型 7種 原始數據類型 1. string?? 2. number?? 3. ??boolean?? 4. null?? 5. undefined?? 6. ??symbol??(ES6 新增) 7. big…

MySQL數據閃回工具my2sql的使用

場景&#xff1a; 當你或者其它人員誤操作數據庫不小心刪除或者更新了一批數據&#xff0c;但是是當時又沒事先備份時&#xff0c;你可以 用這個 my2sql工具快速幫你找回數據。就是如此的絲滑。但是要注意的是只限于dml語句&#xff0c;所以我們在操作數據庫前必需先備份哦&…

9.1無法恢復的錯誤與 panic!

無法恢復的錯誤與 panic! 有時你的代碼中會發生嚴重問題&#xff0c;而你無能為力。在這些情況下&#xff0c;Rust 提供了 panic! 宏。實際上&#xff0c;有兩種方式會導致 panic&#xff1a;一種是執行某個操作使代碼產生 panic&#xff08;例如訪問數組越界&#xff09;&…

分享低功耗單火線開關語音識別方案

在眾多老舊建筑和常規家居環境里&#xff0c;單火線布線是主流方式。單火線語音識別芯片方案通過研發和應用特殊的單火線語音識別芯片&#xff0c;實現設備在單火線供電條件下穩定運行&#xff0c;并精準識別語音指令&#xff0c;為智能家居、智能照明等領域帶來便捷的語音控制…

如何在Windows操作系統上通過conda 安裝 MDAnalysis

MDAnalysis 是一個開源的 Python 庫,旨在提供一個高效且靈活的方式來分析和處理分子動力學(MD)模擬數據。它可以從不同的文件格式中讀取模擬軌跡和結構數據,進行復雜的數據處理和分析,廣泛應用于生物物理學、化學、材料科學等領域。 一、創建虛擬環境 為了能夠順利安裝,減…

實用PDF演示解決方案

它打破了傳統閱 讀模式&#xff0c;讓PDF文檔也能像PPT一樣流暢播放&#xff0c;特別適合匯報、講解等展示場景。它是綠色單文件版&#xff0c;無需安裝&#xff0c;雙擊紅色圖標即點即用。運行后第一件事&#xff0c;建議把界面語言切換成中文&#xff0c;操作更順手。導入PDF…

VS Code中如何關閉Github Copilot

點擊頂部搜索欄后面的Copilot圖標&#xff0c;在下拉菜單中選擇Hide Copilot。在彈出的提示框中&#xff0c;點擊Hide Copilot按鈕就可以了。

MySQL學習從零開始--第六部分

Binlog是什么&#xff1f;有哪幾種格式&#xff1f;推薦使用哪種&#xff0c;為什么 Binlog是什么 Binlog二進制日志是MySQL Server層記錄所有更改數據庫內容的操作日志的二進制文件&#xff0c;如操作UPDATE,DELETE,INSERTBinlog不記錄SELECT&#xff0c;SHOW等查詢操作使主從…

走進computed,了解computed的前世今生

computed&#xff08;計算屬性&#xff09;并不是vue獨創的&#xff0c;而是源自計算機科學和響應式編程的長期發展 計算理論的奠基&#xff1a; 函數式編程的純函數思想&#xff1a;計算屬性的核心特征&#xff08;無副作用、依賴輸入確定輸出&#xff09;直接來源于函數式編程…

Java 23 新特性解析與代碼示例

Java 23 新特性解析與代碼示例 文章目錄Java 23 新特性解析與代碼示例1. 引言2. 正式特性2.1. Markdown文檔注釋 (JEP 467)2.2. 廢棄sun.misc.Unsafe的內存訪問方法以移除 (JEP 471)2.3. ZGC&#xff1a;默認啟用代際模式 (JEP 474)3. 預覽特性3.1. 原始類型在模式、instanceof…

spring boot + mybatis + mysql 只有一個實體類的demo

使用MyBatis進行數據庫操作&#xff0c;配置簡單。主要演示了mybatis可以不用只使用方法名來對應mapper.java和mapper.xml。 目錄結構 pom.xml src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── springbootjdbcweb/ │ │ └── …

iRemovalPro完美繞iCloud插卡打電話,A12+支持iOS 18.1.1

iRemovalPro 專業工具全解析與操作指南 &#xff08;支持iOS 14.0 - 16.6.1&#xff0c;A7-A15芯片設備&#xff09; &#x1f449;下載地址見文末 iRemoval Pro iRemoval 專業版是一款來自外國安全研究員的工具&#xff0c;用來幫助一些人因為忘記自己的ID或者密碼&#xff0c…

安卓SELinux策略語法

目錄前言一、 通用AV規則語法1.1 allow source target:class permissions;1.2 neverallow source target:class permissions;二、type三、attribute四、typeattribute五、alias六、typealias七、init_daemon_domain7.1 init_daemon_domain 宏概述7.2 宏展開與實現7.2.1 展開后規…

vscode cursor配置php的debug,docker里面debug

VSCode PHP調試配置指南 概述 本文介紹如何在VSCode中配置PHP調試環境&#xff0c;包括本地和Docker環境。 前置要求 VSCodePHP 7.0Xdebug擴展PHP Debug VSCode擴展 本地調試配置 1. 安裝Xdebug # Ubuntu/Debian sudo apt-get install php-xdebug# MacOS brew install p…