2019年2月1日 星期五

進一步了解 ReaderWriterLockSlim Mutex Lock 與 執行緒仿射 Thread-affine

進一步了解 ReaderWriterLockSlim Mutex Lock 與 執行緒仿射 Thread-affine

在上一篇文章 為何在使用 ReaderWriterLockSlim 與 非同步 await 關鍵字的時候,會造成系統掛掉 中,我們建立了一個 ASP.NET Core 專案,並且使用了 ReaderWriterLockSlim 來確保對於要寫入到同一個檔案的過程中,可以確保不同執行緒不會同時寫入這個檔案,而造成資料不一致性的問題,可是,卻遇到了專案當掉的問題;我們將會在這篇文章中,簡化這個問題,瞭解出到底發生了甚麼問題,以及更多這方面的問題。
首先,我們建立一個 .NET Core 主控台應用程式,並且填入底下的程式碼:
C Sharp / C#
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 內,取得一個可用的執行緒,繼續來執行。
現在,讓我們來實際執行這段程式碼,看看會得到甚麼樣的輸出內容,以及又會發生甚麼問題呢?
Console
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() 才能夠正常解除封鎖。在這個範例中,因為這兩個敘述分別在不同執行緒下執行,所以,就會造成例外異常。
Console
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 並且解除封鎖。
現在,讓我們來執行看看:
C Sharp / C#
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()分別在不同的執行緒下,因此,將會產生這樣的例外異常錯誤。
Console
Press any key for continuing...
進入 Mutex 封鎖前的 執行緒 : 3
等候非同步工作前的 執行緒 : 3
繼續非同步工作的 執行緒 : 4
進入 Mutex 解除封鎖前的 執行緒 : 4
Console
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



沒有留言:

張貼留言