2018年8月27日 星期一

使用 CancellationTokenSource 的 CancellationToken 於非同步工作中的三種取消請求作法

使用 CancellationTokenSource 的 CancellationToken 於非同步工作中的三種取消請求作法

當我們進行非同步或者多執行緒程式設計的時候,若想要通知非同步的工作或者執行緒終止執行工作或者停止執行,我們可以透過 CancellationTokenSource 類別來完成。在這個類別中有一個方法 CancellationTokenSource.Cancel() ,當我們執行這個方法的時候,就會送出取消執行的訊號到 CancellationToken 物件內。

了解更多關於 [使用 async 和 await 進行非
了解更多關於 [Task Class] 的使用方式
了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式

而當我們建立了 CancellationTokenSource 類別物件之後,最重要的是要把這個 CancellationTokenSource.Token 物件(其型別為 CancellationToken 類別),傳遞到非同步工作方法或者指定的執行緒內,這樣,在這些執行緒內我們就可以使用 CancellationToken 類別所提供的三種方法,提供終止這個非同步程式碼的執行,
這篇文章的範例程式碼,可以從 https://github.com/vulcanlee/CSharpNotes2018/tree/master/TaskCancel取得
其中,我們將會用到這兩個類別的物件,可以從底下文件查看到更多資訊
接下來我們就來瞭解這三種作法的程式碼是如何寫出來的呢?不過,不管是哪種做法,在我們呼叫非同步工作端,我們會進行執行這樣的程式碼 (如下所示):
  • 建立一個 CancellationTokenSource 類別物件
    • 這裡在呼叫建構式的時候,有指定 10 秒鐘的時間,代表當這個非同步工作超過了 10 秒鐘沒有執行完成的話,也會自動產生取消非同步工作執行請求。
  • 建立一個背景執行緒,接受使用者輸入任一鍵盤按鍵,並且執行 CancellationTokenSource.Cancel() 方法,請求取消非同步工作的執行
  • 接著,我們就會等候 await 非同步工作,並且將 CancellationTokenSource.Token 物件傳送到這個非同步工作內
C Sharp / C#
CancellationTokenSource ctsIsCancellationRequested = new CancellationTokenSource(TimeSpan.FromSeconds(10));
ThreadPool.QueueUserWorkItem((s) =>
{
    Console.WriteLine($"(IsCancellationRequested)請按下任一按鍵或者等候10秒鐘,該非同步工作就會取消");
    Console.ReadKey();
    ctsIsCancellationRequested.Cancel();
});
try
{
    await MethodIsCancellationRequestedAsync(ctsIsCancellationRequested.Token);
}
catch (OperationCanceledException oce)
{
    Console.WriteLine($"已經捕捉到使用者發出的工作取消例外異常 {oce.Message}");
}
現在,讓我們來看看這三中做法,其中,我們的非同步工作將會使用 TaskCompletionSource 類別來建立,並且使用 Task.Run 方法,建立一個執行緒,讓這個非同步工作在這個執行緒內來執行。每個執行緒內會執行 100 次的迴圈,每次都會休息 0.5 秒鐘。

使用 IsCancellationRequested 來處理工作取消請求

第一種作法將是採用輪詢 Polling 的方式,隨時檢查 CancellationToken.IsCancellationRequested 這個屬性是否為真,若這個屬性為真,則代表在其他的執行緒上,有程式碼呼叫了 CancellationTokenSource.Cancel() 方法,發出了需要取消非同步工作執行的請求。因此,我們需要經常性地去檢查這個 CancellationToken.IsCancellationRequested 屬性值為真,我們就需要結束這個非同步執行緒的委派方法執行,這樣就會結束 / 取消 非同步工作了。在這裡,我們僅需要 TaskCompletionSource.SetCanceled() 這個方法,告知這個 Task (非同步工作) 物件,該工作已經取消了,在呼叫等候端的程式碼,將會得到 OperationCanceledException 例外異常,我們需要小心處理這個例外異常即可。
C Sharp / C#
private static Task MethodIsCancellationRequestedAsync(CancellationToken token)
{
    TaskCompletionSource<DateTime> tcs = new TaskCompletionSource<DateTime>();
    Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(500);
            Console.Write(".");
            if (token.IsCancellationRequested == true)
            {
                Console.WriteLine($"使用者已經提出取消請求");
                tcs.SetCanceled();
                break;
            }
        }
    });
    return tcs.Task;
}

使用 ThrowIfCancellationRequested 來處理工作取消請求

第二種作法將是採用輪詢 Polling 的方式,不過,我們需要隨時執行 CancellationToken.ThrowIfCancellationRequested() 這個方法,一旦其他的執行緒上,有程式碼呼叫了 CancellationTokenSource.Cancel() 方法,並且我們也執行了 CancellationToken.ThrowIfCancellationRequested() 這個方法,那就會產生出一個 OperationCanceledException 例外異常,若每有發出取消請求,則執行 CancellationToken.ThrowIfCancellationRequested() 這個方法,是不會有任何動作產生的。因此,我們需要經常性地去執行 CancellationToken.ThrowIfCancellationRequested() 這個方法。不過,在這裡要特別注意,因為我們的非同步工作使用 TaskCompletionSource 來設計,因此,若要讓這個 Task 工作物件完成此次非同步執行(最後結果可能是 完成、失敗、取消),我們需要特別指定這個工作是使用甚麼方式來完成,因此,若是因為有取消請求,我們還需要執行 TaskCompletionSource.SetCanceled() 這個方法。
在我們剛剛的設計中,若先執行了 CancellationToken.ThrowIfCancellationRequested() 這個方法,並且已經有 CancellationTokenSource.Cancel() 方法執行了,這個非同步執行緒就會立即結束,但是我們還需要執行 TaskCompletionSource.SetCanceled() 方法,否則,您的程式將會無窮盡的持續等候這個工作結果。因此,我們將 CancellationToken.ThrowIfCancellationRequested() 這個方法 放在一個 try...catch 區段內,若產生了 任何例外異常(當然是要 OperationCanceledException 物件),我們就可以在 catch 區段內執行 TaskCompletionSource.SetCanceled() 這個方法,告知這個 Task (非同步工作) 物件,該工作已經取消了,在呼叫等候端的程式碼,將會得到 OperationCanceledException 例外異常,我們需要小心處理這個例外異常即可。
C Sharp / C#
private static Task MethodThrowIfCancellationRequestedAsync(CancellationToken token)
{
    TaskCompletionSource<DateTime> tcs = new TaskCompletionSource<DateTime>();
    Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(500);
            Console.Write(".");
            #region 不可行的模式
            //// 當收到 Cancel() 方法呼叫,底下的方法會造成例外異常,整個程式會被凍結
            //token.ThrowIfCancellationRequested();
            #endregion

            try
            {
                token.ThrowIfCancellationRequested();
            }
            catch
            {
                // 底下這行會造成在除錯模式下,該執行緒有例外異常中斷發生
                tcs.SetCanceled();
            }
        }
    });
    return tcs.Task;
}

使用 Register 事件 來處理工作取消請求

第三種作法將是採用 CancellationToken.Register 事件來處理,我們需要訂閱這個事件,並且指定一個委派方法,進行當接收到 CancellationTokenSource.Cancel() 方法有執行的通知。這裡需要特別的注意,當 CancellationToken.Register 事件之委派方法被執行的時候,這個時候將會在一個新的執行緒中來執行,也就是與執行非同步工作的執行緒是不同的,因此,我們無法簡單的在 CancellationToken.Register 事件的委派方法內,使用 Thread.CurrentThread.Abort() 這個方法,來讓非同步工作的執行緒中止執行,因為,您將會終止現在正在執行的執行緒,也就是 CancellationToken.Register 事件之委派方法。
因此,解決作法將是,當使用 Task.Run 產生一個非同步執行緒的值後,立即將這個執行緒物件儲存起來,當在 CancellationToken.Register 事件之委派方法被執行的時候,我們可以使用剛剛儲存起來的執行緒物件,呼叫該執行緒的 Abort() 方法,這樣,這個非同步工作執行緒就會終止執行了。在這裡,我們是在 CancellationToken.Register 事件之委派方法 啟用另外一個執行緒,來完成這項工作 ThreadPool.QueueUserWorkItem(x =>{thread.Abort();});
當然,還是不要忘記要執行 TaskCompletionSource.SetCanceled() 這個方法,讓這個 Task 工作物件完成此次非同步執行,其結果是被取消了,最後,在呼叫等候端的程式碼,將會得到 OperationCanceledException 例外異常,我們需要小心處理這個例外異常即可。
C Sharp / C#
private static Task MethodRegisterCallBackAsync(CancellationToken token)
{
    Thread thread;
    bool isCancle = false;
    TaskCompletionSource<DateTime> tcs = new TaskCompletionSource<DateTime>();
    Task.Run(() =>
    {
        thread = Thread.CurrentThread;
        Console.WriteLine($"非同步執行緒 ID = {Thread.CurrentThread.ManagedThreadId}");
        #region Register不正確的作法
        token.Register(() =>
        { 
            Console.WriteLine($"Register事件內 執行緒 ID = {Thread.CurrentThread.ManagedThreadId}");
            // 方法1: 設定要取消旗標為真,讓這個執行緒結束執行
            isCancle = true;

            #region 類另做法,啟動另外一個執行緒,終止非同步執行緒。因為當執行 SetCanceled() 方法後,就無法再執行其他敘述了
            Console.WriteLine($"準備針對非同步工作 執行緒 ID = {thread.ManagedThreadId} 下達終止請求");
            // 方法2: 啟動另外一個執行緒,下達執行緒終止執行的請求
            ThreadPool.QueueUserWorkItem(x =>
            {
                thread.Abort();
            });
            #endregion

            // 下達這個方法,也只是造成非同步工作完成,但原來執行緒上的工作,卻還是繼續在執行
            tcs.SetCanceled();
            // 下達該指令也是無效的,因為,在該事件內與非同步工作內的執行緒 是不同的
            //Thread.CurrentThread.Abort();
        }, true);
        #endregion
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(500);
            Console.Write(".");
            // 方法1: 在這裡判斷要讓這個執行緒結束執行
            //if (isCancle) break;
        }

    });
    return tcs.Task;
}

了解更多關於 [使用 async 和 await 進行非
了解更多關於 [Task Class] 的使用方式
了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式

關於 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月26日 星期日

在 Task.Run 內產生例外異常 Exception,我們可以在呼叫端捕捉到該例外異常嗎?

在 Task.Run 內產生例外異常 Exception,我們可以在呼叫端捕捉到該例外異常嗎?

在這篇文章,我們將要來實驗當我們使用 TaskCompletionSource 設計一個非同步工作方法,並且在這個方法內,使用 Task.Run 方法,從執行緒集區 ThreadPool 取得一個執行緒,便開始執行相關非同步處理工作;在這個非同步工作內,我們先任執行緒休息兩秒鐘,根據傳入進來的參數,決定是要在這個執行緒內直接丟出例外異常,還是要使用 TaskCompletionSource.SetException 方法,來設定這個非同步方法是失敗的,因為有這個例外異常產生出來。

了解更多關於 [Task Class] 的使用方式

首先,我們先將 await MethodAsync("New Exception"); 敘述註解起來,await MethodAsync("TaskCompletionSource.SetException"); 敘述解除註解起,便開始執行該專案,此時該程式的執行結果為
主執行緒 ID : 1
進入 Task.Run 方法內的 執行緒 ID : 5
完成非同步工作,返回的執行緒 ID : 5
藉由SetException 產生例外異常
Press any key for continuing...
看的出來,我們在呼叫端是可以正常捕捉到這個非同步方法內的其他執行緒中所產生的例外異常,並且可以在呼叫端,依據捕捉到的例外異常做出其他相關處理動作。
現在,請將 await MethodAsync("TaskCompletionSource.SetException"); 敘述註解起來,await MethodAsync("New Exception"); 敘述解除註解起,並且開始執行該專案。
現在,我們看到在呼叫這個非同步方法的呼叫端,竟然無法捕捉到非同步工作方法內的例外異常,這是因為,我們使用了 Task.Run 從 ThreadPool 取出一個執行緒來執行,如同我們知道了,若在某一執行緒中,因為設計不良進而導致了例外異常發生,此時,該應用程式將會崩潰掉,這也就是當您直接使用執行緒來設計非同步處理需求的時候,所會面臨到的困境,您需要提升您的程式設計能力與經驗,方能夠避免掉這樣的問題;原則上,我們可以在 Task.Run 內的委派方法內,在最外層就使用 Try ... Catch 將整個非同步程式碼都捕捉起來,若發生了例外異常的話,就使用 TaskCompletionSource.SetException 方法來呼叫即可。
Task.Run Thread Exception


這篇文章的範例程式碼,可以從 https://github.com/vulcanlee/CSharpNotes2018/tree/master/TaskRunException 取得
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        GenerateEmptyThreads();
        try
        {
            Console.WriteLine($"主執行緒 ID : {Thread.CurrentThread.ManagedThreadId} ");
            //await MethodAsync("New Exception");
            await MethodAsync("TaskCompletionSource.SetException");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"完成非同步工作,返回的執行緒 ID : {Thread.CurrentThread.ManagedThreadId} ");
            Console.WriteLine(ex.Message);
        }

        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }

    private static void GenerateEmptyThreads()
    {
        ThreadPool.QueueUserWorkItem((x) =>
        {
            Thread.Sleep(200);
        });
        ThreadPool.QueueUserWorkItem((x) =>
        {
            Thread.Sleep(1200);
        });
    }

    static Task MethodAsync(string generateExceptionAction)
    {
        TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();

        Task.Run(() =>
        {
            Console.WriteLine($"進入 Task.Run 方法內的 執行緒 ID : {Thread.CurrentThread.ManagedThreadId} ");

            Thread.Sleep(1000);
            if (generateExceptionAction == "New Exception")
                throw new Exception("直接拋出 例外異常");
            if (generateExceptionAction == "TaskCompletionSource.SetException")
                tcs.SetException(new Exception("藉由SetException 產生例外異常"));
        });
        return tcs.Task;
    }
}


了解更多關於 [Task 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 課程


TAP Task 產生例外異常 Exception 發生同步或者非同步的探討

TAP Task 產生例外異常 Exception 發生同步或者非同步的探討

我們知道,若直接使用執行緒 Thread 來設計非同步 Asynchronous 處理需求,我們是無法在啟動該執行緒那哩,透過 try { someThread.Star() } catch {...} 這樣的程式碼,捕捉到任何在該執行緒中發生的任何例外異常,您僅能夠在該執行緒方法內自行捕捉相關例外異常事件的產生。
而當我們使用 Task 類別協助我們進行非同步應用程式碼開發的時候,必定會遇到在非同步方法內會發生例外異常 Exception 的問題,而我們必須知道,當您使用 Task 進行設計的時候,在呼叫端是可以捕捉到這些非同步方法內產生的例外異常事件;可以這樣做到的原因在於當我們進行 Task 類別為基礎的非同步方法設計的時候,當有例外異常發生的話,當時發生的例外事件會儲存在 Task 物件內,我們可以透過 Task.Exception 這個物件來取得當時發生例外異常的物件。
不過,當進行 Task 非同步方法呼叫的時候,我們通常會使用 await 關鍵字來等候這個非同步工作完成,因此,當有使用 await 等候非同步工作的時候,若非同步方法內有例外異常產生,這個 await 將會在呼叫非同步方法的呼叫端,再次丟出這個例外異常出來,因此,我們就可以捕捉到。然而,若我們使用類似這樣的敘述 Task<string> task2 = GetContentAsync("https://www.google.com"); 與 var content = await task2;這兩個敘述的時候,會在呼叫非同方法的呼叫端產生例外異常的敘述將會是後者。
另外一個注意事項,那就是在 Task 為基礎的非同步方法內,在該非同步方法內尚未呼叫任何其他非同步方法的時候,例如,我們可能會在非同步工作方法的最前面,要檢查使用者是否有傳入適當的參數到這個非同步方法內,若沒有,我們就會丟出例外異常出來;不過,因為該非同步工作方法尚未進入呼叫與等候其他的非同步工作,因此,我們在呼叫端無需使用 await 關鍵字,當取得這個非同步工作的 Task 物件時候,就會產生了例外異常。我們以這兩行程式碼敘述為例 Task<string> task1 = GetContentAsync(null); 與 var content = await task1;,若符合剛剛敘述的情境,呼叫端的例外異常將會發生在前者敘述。想要測試這樣的過程,請將底下範例程式碼的 #region 在執行非同步方法時發生例外異常 到 #endregion 之間的敘述註解起來,並且在 Task<string> task1 = GetContentAsync(null); 與 Console.WriteLine(exceptionSync.Message); 這兩個敘述上設定一個除錯中斷點;現在,讓我們來實際執行這個範例程式碼。
當程式執行後,會立即停留在 Task<string> task1 = GetContentAsync(null); 方法上,此時,尚未進入到任何非同步工作方法內,現在,我們逐步執行來進行除錯,因為我們傳入一個空值到這個非同步工作內,因此,在這個非同步工作方法內在為呼叫讓和其他非同步工作的時候,就直接產生了例外異常,所以,Task<string> task1 = GetContentAsync(null); 這行敘述雖然僅是取得一個非同步工作 Task 物件,並且我們還沒有使用 await 關鍵字來等候這個非同步工作,就已經在呼叫端產生的例外異常。
現在,我們來看看另外一個情境,請將 #region 在執行非同步方法前發生例外異常 與 #endregion 之間的程式碼註解起來,並且將 #region 在執行非同步方法時發生例外異常 到 #endregion 之間的敘述解除註解,並且在 Task<string> task2 = GetContentAsync("https://www.google.com"); 與 Console.WriteLine(exceptionSync.Message); 這兩個敘述上設定一個除錯中斷點;讓我們來執行這段測試程式碼。
當程式執行後,程式就會停在 第一個中斷點 Task<string> task2 = GetContentAsync("https://www.google.com"); 上,現在我們繼續逐步執行這個除錯程式,我們發現到竟然沒有在呼叫端產生任何例外異常,這是因為當我們執行這個 GetContentAsync 非同步工作方法的時候,是在該方法內呼叫其他非同步工作的時候,才會產生例外異常,因此,我們程式碼可以繼續執行到這個敘述上 var content = await task2;。不過,當我們繼續執行等候這個非同步工作方法的時候,就發生了例外異常,這是因為此非同步工作的 Task 物件內 Exception 屬性上有例外異常物件,並且我們處於等候非同步工作情境下。
Task oject Exception Property
這篇文章的範例程式碼,可以從 https://github.com/vulcanlee/CSharpNotes2018/tree/master/AsyncTaskException 取得
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        #region 在執行非同步方法前發生例外異常
        try
        {
            Task<string> task1 = GetContentAsync(null);
            var content = await task1;
        }
        catch (Exception exceptionSync)
        {
            Console.WriteLine(exceptionSync.Message);
        }
        #endregion

        #region 在執行非同步方法時發生例外異常
        try
        {
            Task<string> task2 = GetContentAsync("https://www.google.com");
            var content = await task2;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        #endregion


        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();

    }

    static Task<string> GetContentAsync(string url)
    {
        if (url == null)
            throw new ArgumentException($"{nameof(url)} 為空值");

        return GetContentInternalAsync(url);
    }

    static async Task<string> GetContentInternalAsync(string url)
    {
        var content = await new HttpClient().GetStringAsync(url);
        throw new HttpRequestException("模擬存取網路異常");
            return content;
        }
}

關於 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 課程