2018年8月11日 星期六

在 async void 方法內,無法捕捉到例外異常 Exception 的情況與解決方法

在 async void 方法內,無法捕捉到例外異常 Exception 的情況與解決方法

在這篇文章,我們將會透過 ILSpy 來進行 .NET 組件反組譯碼,來查看當我們使用了 async void 這樣的非同步方式設計,並且產生了一個例外異常,在我們呼叫這個非同步方法的時候,是無法捕捉到例外異常 Exception 的範例程式碼。
為了要能夠查看我們寫的範例專案的反組譯 IL 碼,我們可以安裝 ICSharpCode.Decompiler 這個 NuGet 套件,這樣,我們就可以在 Visual Studio 2017 內,直接查看到建置完成後的組件 IL 碼。所以,請在您建立的專案中,在 NuGet 安裝視窗中,輸入 ICSharpCode.Decompiler 關鍵字,找出這個套件,我們在這裡將會安裝 搶鮮版 的 4.0.0.4285-beta1 這個版本套件。
Install ICSharpCode.Decompiler NuGet Package
現在,我們完成我們的測試程式碼,我們在主程式將會同步呼叫 AsyncVoidException_Capture() 方法,在這個方法內,我們會呼叫 ThrowExcpetionAsync() 方法,不過,這個方法我們將會使用 async void 來宣告他是個非同步方法。接著,我們使用 try...catch 將會捕捉這個函數 ThrowExcpetionAsync() 會發生的任何例外異常。當您完成這個範例程式碼之後,請在 throw 這個敘述上設定一個中斷點,我們來看看 AsyncVoidException_Capture() 這個方法,是否可以捕捉到 ThrowExcpetionAsync() 方法內所產生的例外異常。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        AsyncVoidException_Capture();
    }

    private static void AsyncVoidException_Capture()
    {
        try
        {
            ThrowExcpetionAsync();
        }
        catch (Exception)
        {
            // 這裡無法捕捉到例外異常
            throw;
        }
    }

    private static async void ThrowExcpetionAsync()
    {
        throw new Exception("Async Void Exception");
    }
}
底下是我們實際執行結果,您會看到,在第 19 行,我們是無法捕捉到這個方法所產生的例外異常,因為,我們所設定的中斷點的敘述並沒有執行到,例外異常直接停在 async void ThrowExcpetionAsync() 方法內。
async void 方法內產生例外異常 Exception
請停止程式執行,使用滑鼠右擊專案節點,選擇 [Open output in ILSpy] 選項。
在 Visual Studio 內,使用 ILSpy 檢查 IL 碼
此時,ILSpy 程式將會啟動執行起來,請先確認左上方紅色方框處,是否勾選 [C#] 選項,並且在左方點選 [Program] 節點,現在,我們就會看到在這個建置完成後的組件內,會有許多編譯器幫我們自動產生的程式碼,也就是右下方紅色方框標示處,從這裡,我們就可以知道為什麼當 async void ThrowExcpetionAsync() 方法發生了例外異常,而 AsyncVoidException_Capture() 方法卻無法捕捉到這個例外異常。
Async Void Method
讓我們在 ILSpy 中,點選左上方下拉選單,切換成為 IL with C# 選項,接著,點選左方清單的 ThrowExcpetionAsync 項目,我們就會看到這個方法的 IL 碼和 C# 程式碼。我們看到左方清單項目中,d__2 類別出現,這個類別將會是由編譯器自動產生的一個類別,這個類別就是一個有限狀態機,他會把這個 ThrowExcpetionAsync 方法內的敘述,使用有限狀態機包裝起來,讓我們可以順利進行非同步的程式碼呼叫。
ILSpy IL with C#
請展開左方類別 d__2 節點,將會看到 MoveNext():void 這個節點,請點選這個節點,您將會看到這個方法 ThrowExcpetionAsync 內的 相關敘述,右方視窗紅色方框標示處,就是這個方法的 throw new Exception("Async Void Exception"); 之 IL 碼。
ILSpy IL with C#
現在,我們將原先的 private static async void ThrowExcpetionAsync() 函式簽章,修改成為同步方法的 private static void ThrowExcpetionAsync() 函式簽章,也就是我們將 async 這個關鍵字移除了。現在,我們可以重新建置這個專案
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        AsyncVoidException_Capture();
    }

    private static void AsyncVoidException_Capture()
    {
        try
        {
            ThrowExcpetionAsync();
        }
        catch (Exception)
        {
            // 這裡無法捕捉到例外異常
            throw;
        }
    }

    private static void ThrowExcpetionAsync()
    {
        throw new Exception("Async Void Exception");
    }
}
好的,讓我們來執行這個同步方法,現在,我們可以看到,在 AsyncVoidException_Capture() 內,確實可以捕捉到 ThrowExcpetionAsync() 所產生的例外異常了。請接著停止執行該專案。
Async Void Method
讓我們回到 ILSpy ,點選功能表 [File] > [Reload] 選項,並且左上方下拉選單,請選擇 [C#] 選項,最後,點選右方的 [Program] 節點,現在,我們可以看到在組件中所產生這樣的同步方法,並沒有被編譯器做額外的內容。
Async Void Method
讓我們在 ILSpy 左上方的下拉選單,切換選擇 [IL with C#],我們可以查看 ThrowExcpetionAsync() 的 IL 碼,也是沒有任何編譯器自動產生的類別與有限狀態機的設定程式碼,這裡將會是很單純的只有丟出一個例外異常而已。
Async Void Method

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程

2018年8月3日 星期五

客製化 SynchronizationContext 來讓不同執行緒 Thread 可以指定特定執行緒來執行委派方法

SynchronizationContext 類別.aspx)可以提供在各種同步處理模式中傳播同步處理內容的基本功能。這通常會在我們進行 GUI 類型應用程式的時候會用到,例如: Win Forms / WPF / Xamarin.Forms 這些類型的專案,會有這樣的需求那是因為,在這裡 GUI 類型的專案中,若想要設定 UI 控制項相關的屬性或者要變更其設定值,此時,您僅能夠在 UI Thread (UI 執行緒,或稱為 Main Thread 主執行緒) 內來執行這些程式碼。不過,有些時候,我們會需要透過多執行緒的程式設計方式,讓許多需要花費比較多時間的方法,在其他執行緒中來執行,不過,當在其他執行緒執行的方法內,有需要設定 UI 相關的屬性,我們就需要讓這些程式碼能夠在 UI 執行緒內來執行,這個時候,我們就可以透過 SynchronizationContext 類別 來滿足這樣需求。

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


了解更多關於 [同步處理原始物件概觀] 的使用方式
由於 Win Forms / WPF / Xamarin.Forms 這些類型的專案內,已經內建這樣的機制,提供您在不同執行緒內,可以指定某些程式碼可以在 UI 執行緒內來執行;而在這篇文章中,我們將會建立一個 Console 類型的專案,並且自訂一個 SynchronizationContext 類別,提供相同的功能。
這篇文章的專案原始碼,可以從這個資料夾 CustomSynchronizationContext 中找到
我們繼承了 SynchronizationContext 類別,建立一個新的 MySynchronizationContext 類別,我們需要 override 覆寫 Send & Post 這兩個方法,前者是負責 會將同步訊息分派至同步處理內容,後者是負責 會將非同步訊息分派至同步處理內容;在這個練習中,我們僅會覆寫 Post 方法內的敘述。另外,我們也需要在這個類別內,建立一個佇列欄位 Queue messagesQueue,這裡將會記錄別的執行緒需要在這個特定執行緒下要執行的委派方法敘述,由於採用佇列資料結構,因此,將會採取先進先出的設計方式。因此,當在別的執行緒內呼叫了 Post 方法後,在這個方法內會使用敘述 messagesQueue.Enqueue(() => codeToRun(state))將要執行的委派方法儲存到佇列欄位內。
當進行多執行緒程式設計的時候,特別需要注意執行緒安全上的考量,因此,我們將會透過 lock (syncHandle) 敘述,確保受到保護的程式碼區段,在同一個時間內,僅會有一個執行緒可以來執行,這點需要特別注意。
最重要的是這個方法 RunMessagePump(),這個方法裡面是一個無窮迴圈,會來檢查佇列欄位內是否有要執行的委派方法,若有的話,則將該委派方法取出來,並且執行這個委派方法。
C Sharp / C#
class MySynchronizationContext : SynchronizationContext
{
    /// <summary>
    /// 待執行的訊息工作佇列
    /// </summary>
    private readonly Queue<Action> messagesQueue = new Queue<Action>();
    /// <summary>
    /// 用於同步處理之鎖定的物件
    /// </summary>
    private readonly object syncHandle = new object();
    /// <summary>
    /// 是否正在執行中
    /// </summary>
    private bool isRunning = true;

    public override void Send(SendOrPostCallback codeToRun, object state)
    {
        throw new NotImplementedException();
    }

    public override void Post(SendOrPostCallback codeToRun, object state)
    {
        lock (syncHandle)
        {
            // 將要處理的訊息工作,加入佇列中
            messagesQueue.Enqueue(() => codeToRun(state));
            SignalContinue();
        }
    }

    /// <summary>
    /// 進入訊息處理的無窮迴圈
    /// </summary>
    public void RunMessagePump()
    {
        while (CanContinue())
        {
            Console.Write(".");
            Action nextToRun = RetriveItem();
            if (nextToRun != null)
                nextToRun();
        }
    }

    /// <summary>
    /// 取出待處理的訊息工作項目
    /// </summary>
    /// <returns></returns>
    private Action RetriveItem()
    {
        lock (syncHandle)
        {
            while (CanContinue() && messagesQueue.Count == 0)
            {
                Monitor.Wait(syncHandle);
            }
            if (isRunning == true)
            {
                return messagesQueue.Dequeue();
            }
            else
            {
                return null;
            }
        }
    }

    /// <summary>
    /// 是否可以繼續執行
    /// </summary>
    /// <returns></returns>
    private bool CanContinue()
    {
        lock (syncHandle)
        {
            return isRunning;
        }
    }

    /// <summary>
    /// 停止 訊息處理的無窮迴圈 執行
    /// </summary>
    public void Cancel()
    {
        lock (syncHandle)
        {
            isRunning = false;
            SignalContinue();
        }
    }

    /// <summary>
    /// 解除鎖定,可以繼續執行
    /// </summary>
    private void SignalContinue()
    {
        Monitor.Pulse(syncHandle);
    }
}
現在,我們來進行這個客製化的 MySynchronizationContext 測試,我們將建立一個這個類別的物件 static MySynchronizationContext ctx = new MySynchronizationContext(); ,並且會先顯示這個程式是在哪個執行緒下運行,將著,使用 MySynchronizationContext.SetSynchronizationContext(ctx); 來設定我們自己設計的 同步處理的內容 synchronisation context。在程式碼最後,使用這個敘述 ctx.RunMessagePump(); 開始進行 訊息處理的無窮迴圈。


在進入無窮迴圈之前,我們會建立兩個執行緒,一個執行緒 將會讀取使用者輸入指定 Console.ReadKey();,若使用輸入 q 則會結束這個程式,在這裡會使用 ctx.Cancel(); 來取消無窮訊息處理迴圈,這樣,程式就會結束了。另外一個執行緒則是會讀取網路上網頁內容;不過,再進行讀取網頁內容的時候,將要使用敘述 ctx.Post(RunDownloadBlogger, null); 指示需要使用主執行緒來進行執行檔案下載的工作。
C Sharp / C#
class Program
{
    static MySynchronizationContext ctx = new MySynchronizationContext();
    static void Main(string[] args)
    {
        Console.Out.WriteLine("Main Thread No {0}", Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine($"設定自製 SynchronizationContext 之前的 SynchronizationContext.Current : {SynchronizationContext.Current?.ToString()}");

        // 設定我們自己設計的 同步處理的內容 synchronisation context
        MySynchronizationContext.SetSynchronizationContext(ctx);

        Console.WriteLine($"設定自製 SynchronizationContext 之後的 SynchronizationContext.Current : {SynchronizationContext.Current?.ToString()}");

        Thread workerThread = new Thread(new ThreadStart(Run));
        workerThread.Start();

        Thread againThread = new Thread(new ThreadStart(() =>
        {
            while (true)
            {
                var foo = Console.ReadKey();
                if (foo.KeyChar == 'a')
                {
                    Run();
                }
                else if (foo.KeyChar == 'q')
                {
                    ctx.Cancel();
                    break;
                };
            }
        }));
        againThread.Start();

        //  開始進行 訊息處理的無窮迴圈
        ctx.RunMessagePump();
    }

    private static void Run()
    {
        Console.Out.WriteLine("Current New Thread No {0}", Thread.CurrentThread.ManagedThreadId);

        // 將要處理的委派方法項目,送入 訊息處理的無窮迴圈 執行
        ctx.Post(RunDownloadBlogger, null);
    }

    private static void RunDownloadBlogger(object state)
    {
        DownloadBlogger();
    }

    static async void DownloadBlogger()
    {
        Console.Out.WriteLine("MainProgram 下載前 on Thread No {0}", Thread.CurrentThread.ManagedThreadId);
        var client = new System.Net.WebClient();
        var webContentHomePage = await client.DownloadStringTaskAsync("https://mylabtw.blogspot.com/");
        Console.Out.WriteLine("下載 {0} 字元", webContentHomePage.Length);
        Console.Out.WriteLine("MainProgram 下載完後 on Thread No {0} ", Thread.CurrentThread.ManagedThreadId);
    }
}

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


了解更多關於 [同步處理原始物件概觀] 的使用方式



關於 Xamarin 在台灣的學習技術資源
Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程


2018年7月17日 星期二

存取應用程式設定和使用者設定 Settings 的資料

有些時候,我們在進行 C# 程式設計的時候,需要產生一些物件,但是,需要由哪個具體實作類別來產生,並不希望在設計時期來指定,而是在應用程式執行的時候,依據當時應用程式的設定參數,來進行決定要由哪個類別來產生特定介面的物件。在這篇文章中,我們將會設計一個 靜態工廠模式 Static Factory Pattern 類別,建立起一個鬆散耦合的程式。
當我們呼叫靜態工廠方法的 StaticFactory.GetMessage() 方法的時候,會依據 應用程式設定和使用者設定 Settings.settings 定義的資料,產生出一個型別為 IMessage 的具體實作類別物件;在這個範例中,我們要產生一個可以發送訊息通知的物件,將所們指定的訊息發送出去,我們設計了介面 IMessage,並且有兩個類別 SMSMessage (使用簡訊方式發送) 與 EMailMessage (使用電子郵件方式發送) 皆實作這個介面,因此,我們在主程式端,透過靜態工廠模式類別,幫助我們產生一個 IMessage 的實作物件,讓我們可以傳送出訊息出去。
不過,我們在靜態工廠模式類別中,並沒有指定要由哪個類別來產生這個物件,而是我們會讀取 應用程式設定和使用者設定 Settings.settings 定義內容,找到 IMessage 的設定值 ( Properties.Settings.Default.IMessage ),這個設定文字的內容是會指定類別的在組件中定義名稱,我們使用這個字串與 Type.GetType 方法,幫助我們產生出指定的型別物件,透過 Activator.CreateInstance 方法,可以產生出該類別的執行個體了,更多資訊,您可以參考 使用應用程式設定和使用者設定
在這裡,我們建立一個 .NET Framework 主控制台應用專案 AccessUserSettings
C Sharp / C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AccessUserSettings
{
    public interface IMessage
    {
        void Send(string message);
    }
    public class SMSMessage : IMessage
    {
        public void Send(string message)
        {
            Console.WriteLine($"簡訊已經送出 : {message}");
        }
    }
    public class EMailMessage : IMessage
    {
        public void Send(string message)
        {
            Console.WriteLine($"郵件已經送出 : {message}");
        }
    }
    public class StaticFactory
    {
        public static IMessage GetMessage()
        {
            string fooValue = Properties.Settings.Default.IMessage;
            Type fooType = Type.GetType(fooValue);
            IMessage fooObject = Activator.CreateInstance(fooType) as IMessage;
            return fooObject;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            IMessage messageObject = StaticFactory.GetMessage();
            messageObject.Send("我已經動態產生具體實作物件了");

            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
}
緊接著,我們在該專案中,使用滑鼠雙擊 Properties 節點,或者查看該專案的屬性,此時,會看到如下圖畫面,請切換到 設定 標籤頁次‵,不過您會看到這樣的訊息: 此專案未包含預設的設定檔,請按一下這裡建立資源檔。請您點選這個藍色文字。
Settings.Settings
現在,您會看到在方案總管中該專案結構上,在 Properties 節點下,出現了一個 Settings.settings 檔案,這個就是我們準備要設定資料的地方。
Settings.Settings
請在該專案屬性頁面上的設定標籤頁次中,依據下圖填入 IMessage 的值為 AccessUserSettings.SMSMessage, AccessUserSettings
Settings.Settings
若想要得知您專案內的某個類別在組件中的表示字串,可以使用底下程式碼來得知。
C Sharp / C#
#region 取得指定完整的類型名稱
var foo = typeof(SMSMessage);
var bar = foo.AssemblyQualifiedName.ToString();
Console.WriteLine(bar);
#endregion
現在,我們執行之後,就會會看到我們產生了類別 SMSMessage 這個類別的執行個體
Console
簡訊已經送出 : 我已經動態產生具體實作物件了
Press any key for continuing...
現在,讓我們來切換當要取得一個 IMessage 介面之具體實作類別物件,不過,需要產生的是 EMailMessage,請在該專案屬性頁面上的設定標籤頁次中,依據下圖填入 IMessage 的值為 AccessUserSettings.EMailMessage, AccessUserSettings
Settings.Settings
現在,我們將會看到我們產生了類別 EMailMessage 這個類別的執行個體
Console
郵件已經送出 : 我已經動態產生具體實作物件了
Press any key for continuing...

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程