2019年2月4日 星期一

C# async 非同步方法需要額外花費多少時間成本之評估


C# async 非同步方法需要額外花費多少時間成本之評估

我們知道,當我們撰寫一個 async 方法的時候,就算我們只有撰寫一行 Console.Write 方法,編譯器也會幫我們產生出許多的 IL ( intermediate language ) 程式碼,也就是說,雖然我們僅有撰寫一行程式碼,面對一個非同步的方法,編譯器幫助我們產生了當要設計出一個非同步方法所需要用到的相關程式碼,這些程式碼主要是由一個狀態機 state machine 所組成,而且,編譯器會依據您正在設計的 async 方法,自動建立出其他的類別代碼;也就是說,當我們呼叫這個非同步方法的時候,將會透過編譯器所產生的程式碼來運作。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [Task Class] 的使用方式

其中第一個就是要進行狀態機的初始化設定工作,在狀態機內將會開始呼叫 MoveNext 方法,開始執行這個非同步方法;在這個狀態機內將會知道當呼叫 await 關鍵字的時候,並且當所呼叫的非同步方法執行完成之後,該繼續從哪裡來執行點,而狀態機內也會有我們所設計的非同步方法的敘述,不過,會依據您所設計的程式碼,拆解成為不同的區塊,但是,您不用擔心,狀態機將會把這些程式碼串接起來,並且也會處理當有例外異常發生的時候,把這些例外異常捕捉起來,記錄在工作物件內。
現在,我們要來看看當我們使用了 async 修飾詞之後,會有甚麼影響?這包含了在 async 方法內沒有使用 await 關鍵字與有使用 await 關鍵字的執行效能。
首先,我們來撰寫個同步方法的呼叫花費時間測試,測試程式碼如下:
在我的電腦上,螢幕輸出結果為 呼叫 1000 次 同步方法 的平均花費時間 : 0.0001087 ms ,好的,我們接下來繼續做下一個測試。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 1000; i++)
        {
            string foo = 同步方法();
        }
        sw.Stop();
        double cost = sw.Elapsed.TotalMilliseconds / 1000.0;
        Console.WriteLine($"呼叫 1000 次 同步方法 的平均花費時間 : {cost} ms");
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static string 同步方法()
    {
        return "同步方法";
    }
}
我們來撰寫個空的非同步方法的呼叫花費時間測試,測試程式碼如下,在這裡的 空的非同步方法() 並沒有做任何事情,如同前面的同步方法,唯一的差異就是我們使用了 async 修飾詞在這個方法上,並也在這個 空的非同步方法() 內,也沒有使用任何的 await 關鍵字。
在我的電腦上,螢幕輸出結果為 呼叫 1000 次 同步方法 的平均花費時間 : 0.0014271 ms ,經過與前面的同步方法來比較,在同步方法前面加上 async 關鍵字之後,每次使用 await 還呼叫這個空的非同步方法,將會多花費 0.0013184ms 時間,也就是成長的 13.13倍,好的,我們接下來繼續做下一個測試。
C Sharp / C#
static async Task Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 1000; i++)
    {
        string foo = await 空的非同步方法();
    }
    sw.Stop();
    double cost = sw.Elapsed.TotalMilliseconds / 1000.0;
    Console.WriteLine($"呼叫 1000 次 同步方法 的平均花費時間 : {cost} ms");
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}
static async Task<string> 空的非同步方法()
{
    return "同步方法";
}
現在,在空的非同步方法內,使用 await Task.Yield() 進行非同步方法的呼叫,讓我們來看看這樣的做法究竟會花費多少時間,測試程式碼如下:
其中,我們使用 await 等候的 Task.Yield 方法,其功能為: 您可以使用await Task.Yield();中非同步的方法,以強制以非同步方式完成的方法,我們是用來模擬一個等候非同步工作,但是又沒有要做甚麼事情的情境。
在我的電腦上,螢幕輸出結果為 呼叫 1000 次 同步方法 的平均花費時間 : 0.361438 ms ,很明顯的,這樣的做法比起空的非同步方法來說,多花費了 0.3600109 ms 時間,也就是成長了 253.27 倍之多。
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 1000; i++)
        {
            string foo = await 立即返回非同步方法();
        }
        sw.Stop();
        double cost = sw.Elapsed.TotalMilliseconds / 1000.0;
        Console.WriteLine($"呼叫 1000 次 同步方法 的平均花費時間 : {cost} ms");
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static async Task<string> 立即返回非同步方法()
    {
        await Task.Yield();
        return "同步方法";
    }
}
透過這樣的測試過程,同步方法的執行效能,比起非同步方法的執行效能,可以說來的快得多,可是,像要設計一個非同步方法,會相當的麻煩與不好維護程式碼,但是透過了 TPL & async & await 的幫助,可以讓我們使用同步程式碼設計邏輯,設計出具有非同步方法應用的成果,不過,我們所要付出的將會是一點點執行效能的犧牲。


了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [Task Class] 的使用方式







2019年2月3日 星期日

打包 Xamarin.Forms 使用的 C# 程式碼片段成為 Visual Studio 2017 的擴充功能

打包 Xamarin.Forms 使用的 C# 程式碼片段成為 Visual Studio 2017 的擴充功能

在過去幾年中,由於從事於 Xamarin.Forms 專案開發與教學的工作,因此,累積了許多開發上使用的程式碼片段,這些程式碼片段大約有27個;而每次進行教學課程的時候,都會要求 Xamarin.Forms 上課學員可以把這些程式碼片段安裝起來,以便可以加快練習專案的開發。然而,幾乎都經常會有許多學員對於使用 Visual Studio 2017 程式碼片段管理員,把這些程式碼片段檔案安裝到 Visual Studio 開發環境上遇到問題,雖然,我都有提供相關作法文件與操作較學影片,不過,還是會有學員設定與使用上發生了問題。
有鑑於此,想說何不自己開發出一個 Visual Studio 擴充功能,如此,可以讓上課學員可以安裝這個擴充功能之後,便可以把這些好用的程式碼開發片段安裝到他們的 Visual Studio 2017 開發環境上;在這裡,我已經做好這個擴充功能,並且上架到 Visual Studio Marketplace 上,有興趣的人可以自行安裝 Xamarin.Forms Snippet for Prism

建立擴充功能專案

那麼,要如何把自己開發的程式碼片段打包成為 Visual Studio 程式碼片段呢?首先,請打開 Visual Studio 2017 ,從功能表中點選 [檔案] > [新增] > [專案],當 [新增專案] 對話窗顯示出來之後,請選擇 [已安裝] > [Visual C#] > [Extensibility] > [VSIX Project] 這個項目,最後,請在對話窗的下方,輸入這個專案的名稱,並且點選 [確定] 按鈕,以便完成建立這個專案。

建立程式碼片段資料夾與設定檔案屬性

請使用滑鼠右擊這個專案,建立底下階層的資料夾 [Snippets] > [CSharp] > [XamarinPrism] ,最後一個資料夾 XamarinPrism 的名稱,您可以依據您的喜好,選擇一個適合的名稱。
此時,您可以把這些程式碼片段,加入倒 [XamarinPrism] 資料夾內,不過,此時,請特別注意要進行底下的設定動作,否則,會有問題產生。
請將這個 Visual Studio 專案內的所有程式碼片段節點全選起來,並且在屬性視窗內,請先選擇這些程式碼片段檔案的 [建置動作] 屬性為 [內容],接著,請設定 [Include in VSIX] 這個屬性為 [True]。

建立與設定機碼內容

請在這個專案內,加入一個 XamarinPrismSnippet.pkgdef 文字檔案,這個檔案的名稱您可以自行設定,並不一定要叫做 XamarinPrismSnippet,接著,請在這個文字檔案內,填入底下內容。
在這裡,我們要設定當這個 Visual Studio 擴充功能在使用者電腦上安裝成功之後,需要在他的電腦上新增底下描述的機碼,在第二行的等號左邊的名稱,將會出現在程式碼片段管理員中的分類名稱,因此,您可以依據您的需要,自行調整與變更成為您需要的名稱。
[$RootKey$\Languages\CodeExpansions\CSharp\Paths]
"XamarinPrism"="$PackageFolder$\Snippets\CSharp\XamarinPrism"

修正 source.extension.vsixmanifest

現在,請在專案中,找到 source.extension.vsixmanifest 節點,使用滑鼠雙擊這個節點,便會看到一個視窗內容,請點選 [Assets] 標籤頁次;接著,請點選右方的 [New] 按鈕。
現在會看到一個 [Add New Asset] 對話窗,請在 [Type] 欄位中,選擇 [Microsoft.VisualStudio.VsPackage],接著在 [Source] 欄位中,選擇 [File on filesystem] 這個選項。
此時,會出現一個新的 [Path] 欄位,我們可以將剛剛建立好的 XamarinPrismSnippet.pkgdef 這個文字檔案名稱,輸入到 [Path] 欄位中 [XamarinPrismSnippet.pkgdef] ,完成後,請點選 [OK] 按鈕。
請在這個視窗的最右上方,看到有兩個欄位,分別是 [Author] 與 [Version] ,請適當修正這兩個欄位值。

進行測試

好的,現在我們可以開始進行這個擴充功能的除錯執行與測試了,現在,請點選工具列上的 Current Instance [Visual Studio Enterprise 2017] 這個項目 (我的電腦上安裝的 Visual Studio 2017 是屬於 Enterprise 版本),現在,將會出現一個獨立環境的 Visual Studio。
請在這個獨立測試環境的 Visual Studio 中,打開程式碼片段管理員對話窗,此時,您應該可以從 CSharp 程式碼片段群組內看到 XamarinPrism 這個選項,那就表示這個擴充功能專案已經正常設計好了。


2019年2月1日 星期五

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

為何在使用 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 特性,使用上要特別注意



進一步了解 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