并發(fā)編程 - 線程同步(七)之互斥鎖Monitor
通過前面對鎖lock的基本使用以及注意事項的學(xué)習(xí),相信大家對鎖的同步機(jī)制有了大致了解,今天我們將繼續(xù)學(xué)習(xí)——互斥鎖Monitor。

lock是C#語言中的關(guān)鍵字,是語法糖,lock語句最終會由C#編譯器解析成Monitor類實現(xiàn)相關(guān)語句。

例如以下lock語句:
lock (obj)
{
//同步代碼塊
}
最終會被解析成以下代碼:
Monitor.Enter(obj);
try
{
//同步代碼塊
}
finally
{
Monitor.Exit(obj);
}
lock關(guān)鍵字簡潔且易于使用,而Monitor類 則功能強(qiáng)大,能夠提供比lock關(guān)鍵字更細(xì)粒度、更靈活的控制以及更多的功能。
因為lock關(guān)鍵字是Monitor類的語法糖,因此lock關(guān)鍵字面臨的問題,Monitor類同樣也會面臨。當(dāng)然也會存在一些Monitor類特有的問題。
下面我們一起詳細(xì)學(xué)習(xí)Monitor類的注意事項以及實現(xiàn)一個簡單的生產(chǎn)者-消費(fèi)者模式示例代碼。
01、避免鎖定值類型
這是因為 Monitor.Enter方法的參數(shù)為Object類型,這就導(dǎo)致如果傳遞值類型會導(dǎo)致值類型被裝箱,進(jìn)而導(dǎo)致線程在已裝箱的對象上獲取鎖,最終線程每次調(diào)用Monitor.Enter方法都在一個完全不同的對象上獲取鎖,導(dǎo)致鎖失效,無法實現(xiàn)線程同步。
看看下面這個代碼示例:
public class LockValueTypeExample
{
private static readonly int _lock = 88;
public void Method1()
{
try
{
Monitor.Enter(_lock);
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"線程 {threadId} 通過 lock(值類型) 鎖進(jìn)入 Method1");
Console.WriteLine($"進(jìn)入時間 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"開始休眠 5 秒");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
finally
{
Console.WriteLine($"開始釋放鎖 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成鎖釋放 {DateTime.Now:HH:mm:ss}");
}
}
}
public static void LockValueTypeRun()
{
var example = new LockValueTypeExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
}
看看執(zhí)行結(jié)果:

可以發(fā)現(xiàn)在釋放鎖的時候拋出異常,大致意思是:“對象同步方法在未同步的代碼塊中被調(diào)用。”,這就是因為鎖定的地方和釋放的地方鎖已經(jīng)不一樣了。
02、小心try/finally
如上面的例子,Monitor.Enter方法是寫在try塊中,試想一下:如果在Monitor.Enter方法之前拋出了異常會怎樣異常?看下面這段代碼:
public class LockBeforeExceptionExample
{
private static readonly object _lock = new object();
public void Method1()
{
try
{
if (new Random().Next(2) == 1)
{
Console.WriteLine($"在調(diào)用Monitor.Enter前發(fā)生異常");
throw new Exception("在調(diào)用Monitor.Enter前發(fā)生異常");
}
Monitor.Enter(_lock);
}
catch (Exception ex)
{
Console.WriteLine($"捕捉到異常:{ex.Message}");
}
finally
{
Console.WriteLine($"開始釋放鎖 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成鎖釋放 {DateTime.Now:HH:mm:ss}");
}
}
}
public static void LockBeforeExceptionRun()
{
var example = new LockBeforeExceptionExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
}
上面代碼是在調(diào)用Monitor.Enter方法前隨機(jī)拋出異常,當(dāng)發(fā)生異常后,可以在釋放鎖的時候和鎖定值類型報了同樣的錯誤,執(zhí)行結(jié)果如下:

這是因為還沒有執(zhí)行鎖定就拋出異常,導(dǎo)致釋放一個沒有鎖定的鎖。
那要如何解決這個問題呢?Monitor類已經(jīng)考慮到了這種情況,并給出了解決辦法——使用Monitor.Enter的第二個參數(shù)lockTaken,當(dāng)獲取鎖定成功則更改lockTaken為true。如此在finally的時候只需要判斷l(xiāng)ockTaken即可決定是否需要執(zhí)行釋放鎖操作,具體代碼如下:
public class LockSolveBeforeExceptionExample
{
private static readonly object _lock = new object();
public void Method1()
{
var lockTaken = false;
try
{
if (new Random().Next(2) == 1)
{
Console.WriteLine($"在調(diào)用Monitor.Enter前發(fā)生異常");
throw new Exception("在調(diào)用Monitor.Enter前發(fā)生異常");
}
Monitor.Enter(_lock,ref lockTaken);
}
catch (Exception ex)
{
Console.WriteLine($"捕捉到異常:{ex.Message}");
}
finally
{
if (lockTaken)
{
Console.WriteLine($"開始釋放鎖 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成鎖釋放 {DateTime.Now:HH:mm:ss}");
}
else
{
Console.WriteLine($"未執(zhí)行鎖定,無需釋放鎖");
}
}
}
}
public static void LockSolveBeforeExceptionRun()
{
var example = new LockSolveBeforeExceptionExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
}
執(zhí)行結(jié)果如下:

03、善用TryEnter
我們知道使用鎖應(yīng)當(dāng)避免長時間持有鎖,長時間持有鎖會阻塞其他線程,影響性能。我們可以通過Monitor.TryEnter指定超時時間,可以看看下面示例代碼:
public class LockTryEnterExample
{
private static readonly object _lock = new object();
public void Method1()
{
try
{
Monitor.Enter(_lock);
Console.WriteLine($"Method1 | 獲取鎖成功,并鎖定 5 秒");
Thread.Sleep(5000);
}
finally
{
Monitor.Exit(_lock);
}
}
public void Method2()
{
Console.WriteLine($"Method2 | 嘗試獲取鎖");
if (Monitor.TryEnter(_lock, 3000))
{
try
{
}
finally
{
}
}
else
{
Console.WriteLine($"Method2 | 3 秒內(nèi)未獲取到鎖,自動退出鎖");
}
}
public void Method3()
{
Console.WriteLine($"Method3 | 嘗試獲取鎖");
if (Monitor.TryEnter(_lock, 7000))
{
try
{
Console.WriteLine($"Method3 | 7 秒內(nèi)獲取到鎖");
}
finally
{
Console.WriteLine($"Method3 |開始釋放鎖");
Monitor.Exit(_lock);
Console.WriteLine($"Method3 |完成鎖釋放");
}
}
else
{
Console.WriteLine($"Method3 | 7 秒內(nèi)未獲取到鎖,自動退出鎖");
}
}
}
public static void LockTryEnterRun()
{
var example = new LockTryEnterExample();
var thread1 = new Thread(example.Method1);
var thread2 = new Thread(example.Method2);
var thread3 = new Thread(example.Method3);
thread1.Start();
thread2.Start();
thread3.Start();
}
執(zhí)行結(jié)果如下:

可以發(fā)現(xiàn)當(dāng)Method1鎖定5秒后,Method2嘗試3秒內(nèi)獲取鎖,結(jié)果并未獲取到自動退出;然后Method3嘗試7秒內(nèi)獲取鎖,結(jié)果獲取到鎖并正確釋放鎖。
04、實現(xiàn)生產(chǎn)者-消費(fèi)者模式
除了上面介紹的方法,Monitor類還有Wait、Pulse、PulseAll等方法。
Wait: 該方法用于將當(dāng)前線程放入等待隊列,直到收到其他線程的信號通知。
Pulse: 該方法用于喚醒等待隊列中的一個線程。當(dāng)一個線程調(diào)用 Pulse 時,它會通知一個正在等待該對象鎖的線程繼續(xù)執(zhí)行。
PulseAll: 該方法用于喚醒等待隊列中的所有線程。
然后我們利用Monitor類的這些功能來實現(xiàn)一個簡單的生產(chǎn)者-消費(fèi)者模式。大致思路如下:
1.首先啟動生產(chǎn)者線程,獲取鎖,然后生成數(shù)據(jù);
2.當(dāng)生產(chǎn)者生產(chǎn)的數(shù)據(jù)小于數(shù)據(jù)隊列長度,則生產(chǎn)一條數(shù)據(jù)同時通知消費(fèi)者線程進(jìn)行消費(fèi),否則暫停當(dāng)前線程等待消費(fèi)者線程消費(fèi)數(shù)據(jù);
3.然后啟動消費(fèi)者線程,獲取鎖,然后消費(fèi)數(shù)據(jù);
4.當(dāng)數(shù)據(jù)隊列中有數(shù)據(jù),則消費(fèi)一條數(shù)據(jù)同時通知生產(chǎn)者線程可以生產(chǎn)數(shù)據(jù)了,否則暫停當(dāng)前線程等待生產(chǎn)者線程生產(chǎn)數(shù)據(jù);
具體代碼如下:
public class LockProducerConsumerExample
{
private static Queue<int> queue = new Queue<int>();
private static object _lock = new object();
//生產(chǎn)者
public void Producer()
{
while (true)
{
lock (_lock)
{
Console.ForegroundColor = ConsoleColor.Red;
if (queue.Count < 3)
{
var item = new Random().Next(100);
queue.Enqueue(item);
Console.WriteLine($"生產(chǎn)者,生產(chǎn): {item}");
//喚醒消費(fèi)者
Monitor.Pulse(_lock);
}
else
{
//隊列滿時,生產(chǎn)者等待
Console.WriteLine($"隊列已滿,生產(chǎn)者等待中……");
Monitor.Wait(_lock);
}
}
Thread.Sleep(500);
}
}
// 消費(fèi)者
public void Consumer()
{
while (true)
{
lock (_lock)
{
Console.ForegroundColor = ConsoleColor.Blue;
if (queue.Count > 0)
{
var item = queue.Dequeue();
Console.WriteLine($"消費(fèi)者,消費(fèi): {item}");
//喚醒生產(chǎn)者
Monitor.Pulse(_lock);
}
else
{
//隊列空時,消費(fèi)者等待
Console.WriteLine($"隊列已空,消費(fèi)者等待中……");
Monitor.Wait(_lock);
}
}
Thread.Sleep(10000);
}
}
}
public static void LockProducerConsumerRun()
{
var example = new LockProducerConsumerExample();
var thread1 = new Thread(example.Producer);
var thread2 = new Thread(example.Consumer);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
執(zhí)行結(jié)果如下:

注:測試方法代碼以及示例源碼都已經(jīng)上傳至代碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

浙公網(wǎng)安備 33010602011771號