2019年1月30日 星期三

為何在使用 ReaderWriterLockSlim 與 非同步 await 關鍵字的時候,會造成系統掛掉

最近遇到一個問題,當我們建立一個 ASP.NET Core 的空白專案的時候,接著,在 Startup 類別中,建立一個靜態 ReaderWriterLockSlim 物件,而我們最終的目的是要能夠在 Configure 方法內,使用 app.Run 來寫入當時的時間到特定文字檔案內,用來記錄這個專案是何時啟用的,因此,將會使用底下的範例程式碼,當要把時間寫入到檔案內的時候,使用 ReaderWriterLockSlim.EnterWriteLock() 方法進行通知其他執行緒,現在這個執行緒需要開始進行鎖定,若同時也有其他的執行緒會用到 ReaderWriterLockSlim ,此時,該執行緒將會進入被 封鎖 Block 的狀態,剛剛那個執行緒呼叫了 ReaderWriterLockSlim.ExitWriteLock(),以便解除執行緒同步的狀態。
C Sharp / C#
public class Startup
{
    static ReaderWriterLockSlim lock1 = new ReaderWriterLockSlim();
    public void ConfigureServices(IServiceCollection services) { }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.Run(async (context) =>
        {
            lock1.EnterWriteLock();
            try
            {
                await File.AppendAllTextAsync("C:\\MyLog.log", DateTime.Now + "\n");
            }
            finally
            {
                lock1.ExitWriteLock();
            }
            await context.Response.WriteAsync("Hello World!");
        });
    }
}
看似一切都很美好,可是,當您啟動該專案之後,卻會得到了底下的錯誤訊息,讓人丈二金剛摸不著頭腦,明明就是按照文件說明的用法,卻是無法使用,這到底發生了甚麼問題呢?
C Sharp / C#
An unhandled exception occurred while processing the request.
UnauthorizedAccessException: Access to the path 'd:\MyLog.log' is denied.
System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)

UnauthorizedAccessException: Access to the path 'd:\MyLog.log' is denied.
System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)
System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)
System.IO.FileStream..ctor(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
System.IO.File.AsyncStreamWriter(string path, Encoding encoding, bool append)
System.IO.File.AppendAllTextAsync(string path, string contents, Encoding encoding, CancellationToken cancellationToken)
System.IO.File.AppendAllTextAsync(string path, string contents, CancellationToken cancellationToken)
WebApplication4.Startup+<>c+<<Configure>b__2_0>d.MoveNext() in Startup.cs
+
                    await File.AppendAllTextAsync("d:\\a.log", DateTime.Now + "\n");
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
想要能夠解決這個問題,你需要明瞭 await 這個非同步關鍵字到底是怎麼運作的以及 執行緒仿射 Thread-affine 這兩個內容。
當您在程式內使用了 await 關鍵字,C# 編譯器將會把你的程式碼拆成兩個區塊,一個是在 await 關鍵字之前要等候的程式碼,也就是呼叫了一個非同步的運算之前的所有程式碼,這些程式碼將會在您呼叫 app.Run 這個委派方法當時的執行緒來執行,這個時候,我們也在這個執行緒內呼叫了 ReaderWriterLockSlim.EnterWriteLock() 方法,取得了一個執行緒同步的專屬使用權,其他的執行緒是無法進入到相同的程式碼區段的;另外一個就是當非同步方法執行完畢之後,在 await 關鍵字之後的所有程式碼,這裡將會拆出成為另外一個程式碼區塊。
由於這是個 Console 類型的應用程式,他的 SynchronizationContext 同步處理內容 並不存在,因此,在 await 之後的程式碼將會在另外一個執行緒中來運行(實際上,會從 ThreadPool 執行緒集區中,要求一個可用的執行緒來執行這些程式碼)。
但是,這樣為什麼會造成這個專案發生例外異常呢?這個時候,就需要來了解 執行緒仿射 Thread-affine,當想要使用 ReaderWriterLockSlim 類別作為執行緒同步問題的解決方案的時候, ReaderWriterLockSlim.EnterWriteLock() 與 ReaderWriterLockSlim.ExitWriteLock() 必須要同一個執行緒下來執行。在 UnauthorizedAccessException 說明文件中,可以看到這個例外異常是這個意思:當作業系統因為 I/O 錯誤或特定類型的安全性錯誤而拒絕存取時,所擲回的例外狀況。
因此,我們需要讓執行 await 關鍵字前後的程式區段都能夠在同一個執行緒之下,才能夠避免這樣的問題,這裡提出兩個解決方案,那就是在 app.Run 內的委派方法,不要使用 await 非同步的方式來執行,而是使用同步的方式來執行: File.AppendAllTextAsync(@"d:\\a.log", DateTime.Now + "\n").Wait();,如同底下的程式碼
C Sharp / C#
public class Startup
{
    static ReaderWriterLockSlim lock1 = new ReaderWriterLockSlim();
    public void ConfigureServices(IServiceCollection services) { }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.Run(async (context) =>
        {
            lock1.EnterWriteLock();
            try
            {
                 File.AppendAllTextAsync(@"d:\\a.log", DateTime.Now + "\n").Wait();
            }
            finally
            {
                lock1.ExitWriteLock();
            }
            await context.Response.WriteAsync("Hello World!");
        });
    }
}
第二種作法則是使用 Nito.AsyncEx 這個套件,裡面有提供非同步運算可以使用的 AsyncReaderWriterLock ,這樣當我們使用 await 進行非同步計算的時候,就不會產生問題了。
C Sharp / C#
public class Startup
{
    static AsyncReaderWriterLock lock1 = new AsyncReaderWriterLock();
    public void ConfigureServices(IServiceCollection services)
    {
    }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.Run(async (context) =>
        {
            using (await lock1.WriterLockAsync())
            {
                await File.AppendAllTextAsync(@"D:\Vulcan\Projects\ReaderWriterLockSlimAsyncTest\tmp.txt", DateTime.Now + "\n")
                  ;
            }
            await context.Response.WriteAsync("Hello World!");
        });
    }
}
Monitors (and locks) / Reader-writer locks / Mutex 也同樣的具有 執行緒仿射 Thread-affine 特性,使用上要特別注意


沒有留言:

張貼留言