進一步了解 ReaderWriterLockSlim Mutex Lock 與 執行緒仿射 Thread-affine
在上一篇文章 為何在使用 ReaderWriterLockSlim 與 非同步 await 關鍵字的時候,會造成系統掛掉 中,我們建立了一個 ASP.NET Core 專案,並且使用了 ReaderWriterLockSlim 來確保對於要寫入到同一個檔案的過程中,可以確保不同執行緒不會同時寫入這個檔案,而造成資料不一致性的問題,可是,卻遇到了專案當掉的問題;我們將會在這篇文章中,簡化這個問題,瞭解出到底發生了甚麼問題,以及更多這方面的問題。
首先,我們建立一個 .NET Core 主控台應用程式,並且填入底下的程式碼:
class Program
{
static ReaderWriterLockSlim lock1 = new ReaderWriterLockSlim();
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(AsyncMethod);
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
}
private static async void AsyncMethod(object state)
{
Console.WriteLine($"進入封鎖前的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
lock1.EnterWriteLock();
try
{
Console.WriteLine($"等候非同步工作前的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
await File.AppendAllTextAsync(@"D:\Vulcan\Projects\ThreadAffine\ThreadAffine\MyLog.log", DateTime.Now + "\n");
Console.WriteLine($"繼續非同步工作的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
}
finally
{
Console.WriteLine($"準備進入解除封鎖前 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
lock1.ExitWriteLock();
Console.WriteLine($"準備進入解除封鎖後 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
}
Console.WriteLine("Hello World!");
}
}
在這個測試程式碼中,一樣先產建立一個 ReaderWriterLockSlim 物件,接著呼叫 EnterWriteLock() 方法,準備進入寫入封鎖模式,禁止其他執行緒可以進入到寫入模式內,並且使用 await 關鍵是進行非同步運算,最後,呼叫 ExitWriteLock() 方法,解除寫入封鎖模式。
不過,在這個測試程式碼中,將會在不同的關鍵點,輸出當時 .NET 受管理的執行緒 ID值,用來判斷整段程式碼是否都在同一個執行緒來執行。當然,若您瞭解了 async / await 的運作模式,就會知道,當程式碼執行到 await 非同步方法的時候,就會進入到等候狀態下;當非同步方法執行完畢之後,就會緊接著剛剛等候的程式碼點,繼續接著執行,不過,因為這是一個 Console 模式的應用程式,並沒有 SynchronizationContext 同步內容存在,所以,在 await 等候之後的程式碼,將會從 執行緒集區 ThreadPool 內,取得一個可用的執行緒,繼續來執行。
現在,讓我們來實際執行這段程式碼,看看會得到甚麼樣的輸出內容,以及又會發生甚麼問題呢?
Press any key for continuing...
進入封鎖前的 執行緒 : 3
等候非同步工作前的 執行緒 : 3
繼續非同步工作的 執行緒 : 4
準備進入解除封鎖前 執行緒 : 4
當程式一執行的時候,我們得到到一個例外異常,例外異常訊息內容為:
System.Threading.SynchronizationLockException: 'The write lock is being released without being held.'
,並且,整個專案是在 lock1.ExitWriteLock();
敘述中發生了例外異常。
透過專案執行結果的輸出內容,我們得知道,這個專案一開始執行的時候,是在 執行緒 ID=3 下來執行程式碼,不過,當執行 await 等候之後,這個測試程式碼繼續執行的時候,切換到了 執行緒 ID=4,所以,當該執行緒執行到
lock1.ExitWriteLock()
敘述的時候,發現到在 執行緒 ID=4 內,並沒有執行過 lock1.EnterWriteLock()
敘述,因此,就會拋出例外異常,這是因為 ReaderWriterLockSlim 具備了 執行緒仿射 Thread-affine 特性,必須要在同一個執行緒下先執行 lock1.EnterWriteLock()
,接著再呼叫 lock1.ExitWriteLock()
才能夠正常解除封鎖。在這個範例中,因為這兩個敘述分別在不同執行緒下執行,所以,就會造成例外異常。System.Threading.SynchronizationLockException
HResult=0x80131518
Message=The write lock is being released without being held.
Source=System.Private.CoreLib
StackTrace:
at System.Threading.ReaderWriterLockSlim.ExitWriteLock()
at ThreadAffine.Program.<AsyncMethod>d__2.MoveNext() in D:\Vulcan\Projects\ThreadAffine\ThreadAffine\Program.cs:line 57
那麼,如同前一篇文章提到的:
Monitors (and locks) / Reader-writer locks / Mutex 也同樣的具有 執行緒仿射 Thread-affine 特性
這句話,我們撰寫了底下另外一個測試範例,在這裡,則會使用 Mutex 這個執行緒同步 物件。
現在,呼叫了
mut.WaitOne()
要進入 Mutex 並進入鎖定模式,緊接著執行非同步的檔案寫入,因此,透過這樣的機制,在同一個時間點,僅會有一個執行緒可以寫入到這個檔案內,當檔案寫入完成之後,在這裡將會呼叫 mut.ReleaseMutex()
方法來離開 Mutex 並且解除封鎖。
現在,讓我們來執行看看:
class Program
{
static object myLocker;
private static Mutex mut = new Mutex();
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(AsyncMethod);
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
}
private static async void AsyncMethod(object state)
{
Console.WriteLine($"進入 Mutex 封鎖前的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
mut.WaitOne();
Console.WriteLine($"等候非同步工作前的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
await File.AppendAllTextAsync(@"D:\Vulcan\Projects\ThreadAffine\ThreadAffine\MyLog.log", DateTime.Now + "\n");
Console.WriteLine($"繼續非同步工作的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"進入 Mutex 解除封鎖前的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
mut.ReleaseMutex();
Console.WriteLine($"進入 Mutex 解除封鎖後的 執行緒 : {Thread.CurrentThread.ManagedThreadId}");
}
}
從執行後的輸出結果,將會看到僅有底下四行文字內容輸出,並且,程式在
mut.ReleaseMutex()
這行敘述上發生了例外異常,他的例外異常詳細內容如下所示,而異常訊息為 Object synchronization method was called from an unsynchronized block of code.
,這說明了這個程式呼叫 WaitOne()
& ReleaseMutex()
分別在不同的執行緒下,因此,將會產生這樣的例外異常錯誤。Press any key for continuing...
進入 Mutex 封鎖前的 執行緒 : 3
等候非同步工作前的 執行緒 : 3
繼續非同步工作的 執行緒 : 4
進入 Mutex 解除封鎖前的 執行緒 : 4
System.ApplicationException
HResult=0x80131600
Message=Object synchronization method was called from an unsynchronized block of code.
Source=System.Private.CoreLib
StackTrace:
at System.Threading.Mutex.ReleaseMutex()
at ThreadAffine.Program.<AsyncMethod>d__3.MoveNext() in D:\Vulcan\Projects\ThreadAffine\ThreadAffine\Program.cs:line 31