程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET 同步與異步之鎖(Lock、Monitor)(七),lockmonitor

.NET 同步與異步之鎖(Lock、Monitor)(七),lockmonitor

編輯:關於.NET

.NET 同步與異步之鎖(Lock、Monitor)(七),lockmonitor


本隨筆續接:.NET同步與異步之相關背景知識(六)

 

在上一篇隨筆中已經提到、解決競爭條件的典型方式就是加鎖 ,那本篇隨筆就重點來說一說.NET提供的最常用的鎖 lock關鍵字 和 Monitor。

一、lock關鍵字Demo

        public object thisLock = new object();
        private long index;

        public void AddIndex()
        {
            lock (this.thisLock)
            {
                this.index++;

                if (this.index > long.MaxValue / 2)
                {
                    this.index = 0;
                }
         // 和 index 無關的大量操作 } } public long GetIndex() { return this.index; }

 

這一組demo,代碼簡潔,邏輯簡單,一個 AddIndex 方法 保證字段 index 在 0到100之間,另外一個GetIndex方法用來獲取字段index的值。

但是,這一組Demo卻有不少問題,甚至可以說是錯誤,下面我將一一進行說明:

1、忘記同步——即讀寫操作都需要加鎖

  GetIndex方法, 由於該方法沒有加鎖,所以通過該方法在任何時刻都可以訪問字段index的值,也就是說會恰好在某個時間點獲取到 101 這個值,這一點是和初衷相違背的。

 

2、讀寫撕裂

  如果說讀寫撕裂這個問題,這個demo可能不是很直觀,但是Long類型確實存在讀寫撕裂。比如下面的例子:

/// <summary> /// 測試原子性 /// </summary> public void TestAtomicity() { long test = 0; long breakFlag = 0; int index = 0; Task.Run(() => { base.PrintInfo("開始循環 寫數據"); while (true) { test = (index % 2 == 0) ? 0x0 : 0x1234567890abcdef; index++; if (Interlocked.Read(ref breakFlag) > 0) { break; } } base.PrintInfo("退出循環 寫數據"); }); Task.Run(() => { base.PrintInfo("開始循環 讀數據"); while (true) { long temp = test; if (temp != 0 && temp != 0x1234567890abcdef) { Interlocked.Increment(ref breakFlag); base.PrintInfo($"讀寫撕裂: { Convert.ToString(temp, 16)}"); break; } } base.PrintInfo("退出循環 讀數據"); }); } 測試原子性操作

64位的數據結構是需要兩個命令來實現讀寫操作的,也就是說、如果恰好在兩個寫命令中間發生了讀取操作,就有可能讀取到不完成的數據。故而要警惕讀寫撕裂。

 

3、粒度錯誤

  AddIndex 方法中,和 index 無關的大量操作 ,放在鎖中是沒有必要的,雖然沒必要但是也不是錯的,只能說這個鎖的粒度過大,造成了沒必要的並發上的性能影響。

下面舉例一個錯誤的鎖粒度:

public class BankAccount { private long id; private decimal m_balance = 0.0M; private object m_balanceLock = new object(); public void Deposit(decimal delta) { lock (m_balanceLock) { m_balance += delta; } } public void Withdraw(decimal delta) { lock (m_balanceLock) { if (m_balance < delta) throw new Exception("Insufficient funds"); m_balance -= delta; } } public static void ErrorTransfer(BankAccount a, BankAccount b, decimal delta) { a.Withdraw(delta); b.Deposit(delta); } public static void Transfer(BankAccount a, BankAccount b, decimal delta) { lock (a.m_balanceLock) { lock (b.m_balanceLock) { a.Withdraw(delta); b.Deposit(delta); } } } public static void RightTransfer(BankAccount a, BankAccount b, decimal delta) { if (a.id < b.id) { Monitor.Enter(a.m_balanceLock); // A first Monitor.Enter(b.m_balanceLock); // ...and then B } else { Monitor.Enter(b.m_balanceLock); // B first Monitor.Enter(a.m_balanceLock); // ...and then A } try { a.Withdraw(delta); b.Deposit(delta); } finally { Monitor.Exit(a.m_balanceLock); Monitor.Exit(b.m_balanceLock); } } } 錯誤的鎖粒度

在 ErrorTransfer 方法中,在轉賬的兩個方法中間的時間點上,轉賬金額屬於無主狀態,這時鎖的粒度就過小了 。

在 Transfer 方法中,雖然粒度正確了,但是此時容易死鎖。而比較恰當的方式可以是:RightTransfer 。

 

4、不合理的lock方式

鎖定非私有類型的對象是一種危險的行為,因為非私有類型被暴露給外界、外界也可以對被暴露的對象進行加鎖,這種情況下很容造成死鎖 或者 錯誤的鎖粒度。

較為合理的方式是 將 thislock 改為 private .

由上述進行類推:

1、lock(this):如果當前類型為外界可訪問的也會有類似問題。

2、lock(typeof(T)): 因為Type對象,是整個進程域中是唯一的。所以,如果T為外界可訪問的類型也會有類似問題。

3、lock("字符串"):因為String類型的特殊性(內存駐留機制),多個字符串其實有可能是同一把鎖,所以、一不小心就容易掉入陷阱、造成死鎖 或者錯誤的鎖粒度。

 

二、通過 IL 代碼看本質

 下面是 AddIndex 方法的全部il代碼 [使用 .NET 4.5類庫,VS2015 編譯]:

.method public hidebysig instance void AddIndex() cil managed { // 代碼大小 81 (0x51) .maxstack 3 .locals init ([0] object V_0, [1] bool V_1, [2] bool V_2) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld object ParallelDemo.Demo.LockMonitorClass::thisLock IL_0007: stloc.0 IL_0008: ldc.i4.0 IL_0009: stloc.1 .try { IL_000a: ldloc.0 IL_000b: ldloca.s V_1 IL_000d: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) IL_0012: nop IL_0013: nop IL_0014: ldarg.0 IL_0015: ldarg.0 IL_0016: ldfld int64 ParallelDemo.Demo.LockMonitorClass::index IL_001b: ldc.i4.1 IL_001c: conv.i8 IL_001d: add IL_001e: stfld int64 ParallelDemo.Demo.LockMonitorClass::index IL_0023: ldarg.0 IL_0024: ldfld int64 ParallelDemo.Demo.LockMonitorClass::index IL_0029: ldc.i8 0x3fffffffffffffff IL_0032: cgt IL_0034: stloc.2 IL_0035: ldloc.2 IL_0036: brfalse.s IL_0042 IL_0038: nop IL_0039: ldarg.0 IL_003a: ldc.i4.0 IL_003b: conv.i8 IL_003c: stfld int64 ParallelDemo.Demo.LockMonitorClass::index IL_0041: nop IL_0042: nop IL_0043: leave.s IL_0050 } // end .try finally { IL_0045: ldloc.1 IL_0046: brfalse.s IL_004f IL_0048: ldloc.0 IL_0049: call void [mscorlib]System.Threading.Monitor::Exit(object) IL_004e: nop IL_004f: endfinally } // end handler IL_0050: ret } // end of method LockMonitorClass::AddIndex IL

 當然你沒必要完全看懂,你只需要注意到三個細節就可以了:

1、調用 [mscorlib]System.Threading.Monitor::Enter(object, bool&) 方法,其中第二個入參為 索引為1的local變量 [查類庫後發現該參數是 ref 傳遞引用]。

2、如果索引為1的local變量 不為 false,則 調用 [mscorlib]System.Threading.Monitor::Exit(object) 方法

3、try... finally 語句塊

換句話,也就是說 lock關鍵字其實本質上就是 Monitor 類的簡化實現方式,為了安全、進行了try...finally處理。

 

三、Monitor 的 wait 和 Pulse 

因為進入鎖(Enter)和離開鎖(Exit)都是有一定的性能損耗的,所以,當有頻繁的沒有必要的鎖操作的時候,性能影響更大。

比如:在生產者消費者模式中,如果沒有需要消費的數據時,對鎖的頻繁操作是沒有必要的(輪詢模式,不是推送)。

在這種情況下, wait方法就派上用場了。如下是MSDN中的一句備注:

當前擁有對指定對象的鎖的線程調用此方法以釋放該對象,以便另一個線程可以訪問它。 等待重新獲取鎖時阻止調用方。 當調用方需要等待另一個線程操作後將發生狀態更改時,調用此方法。

 

wait 和  pulse 方法一筆帶過,這對方法、筆者用的也不多。

 

 

隨筆暫告一段落、下一篇隨筆介紹: 鎖(ReaderWriterLockSlim)(預計1篇隨筆)

附,Demo : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip

參見更多:隨筆導讀:同步與異步


(未完待續...)

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved