2022年7月29日 星期五

MAUI : 資料綁定 Data Binding - 1 自行建置 INotifyPropertyChanged 介面

資料綁定 Data Binding - 1 自行建置 INotifyPropertyChanged 介面

當在學習 毛伊 MAUI 跨平台開發工具來進行 Android , iOS , WinUI 等平台應用程式開發的時候,對於 MVVM Model View ViewModel 這樣的設計模式一定十分孰悉與精通 ,而在 MVVM 設計模式下的 資料綁定 Data Binding 觀念與實作技術,也務必要徹底了解,這樣的技術是怎麼運行起來的,可以提供甚麼樣的好處。

資料綁定 Data Binding 的技術背後將會需要實作出 INotifyPropertyChanged 介面,這個型別早就已經存在於 .NET Framework 2.0 內了,所以,這並不是甚麼十分嶄新的技術與觀念,為了要能夠體會與理解 INotifyPropertyChanged 介面,現在就實際來動手實作看看。

系列文章清單

1 自行建置 INotifyPropertyChanged 介面

2 設計基底類別,透過繼承可以使用屬性變更通知

3 PropertyChanged.Fody 套件,大幅簡化屬性變更通知程式設計碼

4 CommunityToolkit.Mvvm 套件,透過原始碼產生來簡化屬性變更通知程式設計碼

5 在 Maui 專案內,如何得知 ViewModel 內的屬性產生異動,而 View 可以收到通知呢?

實作 INotifyPropertyChanged 介面

  • 開啟 Visual Studio 2022 開發工具

  • 當 [Visual Studio 2022] 對話窗出現的時候

  • 點選右下角的 [建立新的專案] 按鈕選項

  • 現在將看到 [建立新專案] 對話窗

  • 請選擇 [主控台應用程式] 這個專案範本

  • 點選右下角的 [下一步] 按鈕

  • 此時將會看到 [設定新的專案] 對話窗

  • 在 [專案名稱] 欄位,輸入 csINotifyPropertyChanged

  • 點選右下角的 [下一步] 按鈕

  • 最後會看到 [其他資訊] 對話窗

  • 請勾選 [不要使用最上層語句] 這個文字檢查盒控制項

  • 點選右下角的 [建立] 按鈕

為了要簡化過程,因此,將會把相關程式碼都寫入到 Program.cs 這個檔案內,當然,也可以把等下要建立的新類別 [Person] 建立到新的 [Person.cs] 檔案內。

  • 請在 [Program.cs] 檔案內 namespace csINotifyPropertyChanged 這個命名空間宣告的最前面,加入一個 [Person] 類別宣告,程式碼如下:
public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

在這個 [Person] 類別內,將需要實作 INotifyPropertyChanged 這個介面,在這個 INotifyPropertyChanged 介面內,只有宣告一個 event PropertyChangedEventHandler? PropertyChanged; 成員,因此,這裡將會加入這樣的 public event PropertyChangedEventHandler? PropertyChanged; 程式碼宣告

在剛剛加入的 [PropertyChanged] 事件成員,將會用來讓外部參考與使用這個類別執行個體的其他物件,可以訂閱 [PropertyChanged] 這個事件,而當這個 [Person] 類別的內部狀態 (相關 欄位 Field) 有變動的時候,將會觸發這個 [PropertyChanged] 事件,如此,便可以讓有訂閱這個事件的其他物件,可以收到此執行個體的狀態有變更的通知,這就是 INotifyPropertyChanged 這個介面最主要的價值與目的。

為了要方便隨時可以觸發 [PropertyChanged] 事件,因此,在這裡設計了一個 [OnPropertyChanged] 方法,該方法可以接收一個字串參數,這個字串參數表示了是哪個屬性名稱發生了異動,而在這個 [OnPropertyChanged] 方法內,僅有一行敘述,這樣的寫法是為了要避免在多執行緒程式碼執行下,具有執行緒安全的特性。

好了,有了基本的 INotifyPropertyChanged 實作程式碼,現在可以來進行這個 [Person] 類別的成員設計,在這個類別內,其實僅設計兩個成員,一個是具有字串型別的 [Name] 屬性,表示這個 Person 物件的姓名,另外一個是具有整數型別的 [Age] 屬性,表示這個 Person 物件的年紀。

不過,為了能夠做出,當這兩個屬性值發生異動的時候,可以觸發 [PropertyChanged] 事件,在這裡是不能夠使用 自動實作的屬性 ,而是要使用 屬性 的方式來設計,也就是要使用 set 與 get 存取子和一個欄位成員來定義出這個屬性。

底下將會是 [Person] 這個類別內,準備要新加入的兩個屬性定義程式碼

public class Person : INotifyPropertyChanged
{

   ...

    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            OnPropertyChanged("Name");
        }
    }

    private int age;
    public int Age
    {
        get { return age; }
        set
        {
            age = value;
            OnPropertyChanged("Age");
        }
    }
}

在此,將先採用姓名這個屬性來說明設計過程,首先,宣告一個 欄位 成員,使用 private string name; 這樣程式碼,很清楚的,因為這是個欄位,因此,需要加入 private 修飾詞,避免外部其他物件來存取,僅供這個類別所建立的執行個體內的程式碼可以存取。

緊接著使用 含有支援欄位的屬性 來設計一個 Name 新屬性。對於這個新的 Name 屬性,其 get 存取子 將會直接回傳 name 這個欄位值,而對於 set 這個存取子,除了一定要將傳入進來的值,設定給 name 這個欄位之外,還需要使用 OnPropertyChanged("Name"); 敘述來觸發該屬性值已經發生變更的行為通知。

同樣的,也需要設計一個 Age 屬性與 age 欄位,參考上述 姓名 的做法,設計出年紀的屬性程式碼。

現在可以開始來體驗 INotifyPropertyChanged 這個介面所帶來的好處與方便性

現在,請在 Main 方法內,加入底下程式碼

Person person = new Person();
person.PropertyChanged += (s, e) =>
{
    WriteLine($"屬性 {e.PropertyName} 已經變更");
};
WriteLine("準備要修改 Name 屬性值");
person.Name = "Vulcan Lee";
WriteLine("Press any key for continuing...");
ReadKey();
WriteLine("準備要修改 Age 屬性值");
person.Age = 25;
WriteLine("Press any key for continuing...");
ReadKey();

在這裡先建立一個型別為 [Person] 的物件,指派給 person 變數內

接著,因為每個 [Person] 物件,都有個公開的 [PropertyChanged] 事件,因此,在此將需要訂閱這個事件,在此使用 Lambda 運算式 設計一個匿名委派方法來綁定這個事件,如此,當有屬性變更的事件產生的時候,將會觸發這裡綁定的 Lambda 委派方法,也就是會在螢幕上顯示出哪個屬性值已經變更了。

完成的變更屬性的事件訂閱與綁定設計,接下來就是要開始針對這個 person 物件的兩個屬性值進行變動,看看是否會有屬性變動通知事件產生,底下是執行後的結果內容。

這裡將會是整個完整的測試程式碼

global using static System.Console;
using System.ComponentModel;

namespace csINotifyPropertyChanged
{
    public class Person : INotifyPropertyChanged
    {
        #region 實作出 INotifyPropertyChanged 的程式碼
        // 在這個 INotifyPropertyChanged 介面內,只有宣告一個 event PropertyChangedEventHandler? PropertyChanged; 成員
        public event PropertyChangedEventHandler? PropertyChanged;
        // 設計底下的方法,是要方便可以觸發 PropertyChanged 這個事件
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

        private int myVar;

        public int MyProperty
        {
            get { return myVar; }
            set { myVar = value; }
        }

        #region 針對每個具有 PropertyChanged 的屬性,都需要有底下的程式碼設計方式

        #region 姓名
        private string name;

        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                OnPropertyChanged("Name");
            }
        }
        #endregion

        #region 年紀
        private int age;

        public int Age
        {
            get { return age; }
            set
            {
                age = value;
                OnPropertyChanged("Age");
            }
        }

        #endregion
        #endregion
    }


    internal class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
            person.PropertyChanged += (s, e) =>
            {
                WriteLine($"屬性 {e.PropertyName} 已經變更");
            };

            WriteLine("準備要修改 Name 屬性值");
            person.Name = "Vulcan Lee";

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

            WriteLine("準備要修改 Age 屬性值");
            person.Age = 25;

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






2022年7月28日 星期四

C# : 當要進行背景服務程式碼設計的時候,是否要使用 await 運算子呢?

當要進行背景服務程式碼設計的時候,是否要使用 await 運算子呢?

這篇文章是記錄我之前在進行 ASP.NET Core 中使用託管服務的背景工作 程式碼設計的時候,想要設計一個背景服務,這個程式將會每隔一段時間來檢查是否有使用者的帳號已經密碼過期,或者需要強制登出的請求出現,此時,這個背景服務程式將會設定這個帳號需要強制登出;若是帳號密碼過期的問題,則該帳號再重新登入的時候,將會要求重新變更帳號密碼,否則無法進行登入。

對於這樣的需求,一開始的想法是,我需要自己 new Thread 這個執行個體出來,產生出一個前景執行緒,因為,在這裡需要進行長時間的處理工作。另外,不去使用執行緒集區來取得一個被景執行緒的原因,那就是這個執行緒會長時間運行,若該執行緒來自於執行緒集區,則該集區內的執行緒便會少了一個可用執行緒資源。

然而,這樣的設計理念雖然很好,很快地就遇到問題,那就是在這個前景執行緒中,因為某些需要,便要呼叫一些非同步的 API ,而這些 API 是可以適用於 async / await 的方式,理所當然的就直接使用這樣的作法把他呼叫下去。

當專案設計完成與正式執行之後,也沒有發現到甚麼異常現象產生,畢竟,在這個前景執行緒的程式碼內,比較沒有存取共用資源的競賽問題,也就是沒有執行緒安全的問題產生。可是,總是感覺怪怪的,畢竟,大家都知道,當程式碼執行到 await 運算子這個關鍵字後,將會做三件事情

  1. 記住離開的執行程式碼位置
  2. 捕捉當前執行內容
  3. 立即 return 返回。

當 await 所等待的這個非同步工作或者方法完成之後,將會做三件事情

  1. 從工作排程器取得一個可以繼續執行緒
  2. 還原之前執行內容
  3. 從剛剛中斷的地方繼續往下執行。

一旦看完剛剛所提到的事情之後,那麼一開始建立一個前景執行緒的動作不就白做了嗎?

因為只要在這個前景執行緒所指定的 Unit Of Work 工作單元 / 委派方法內,使用 await 關鍵字,並且在這個委派方法外加入 async 關鍵字,那麼,執行完成 await 關鍵字之後,前景執行緒也就不再存在了,之後的程式碼都會來自於 工作排成器 Task Scheduler 預設將會從 執行緒集區 取得一個可用的執行緒來繼續執行下去。

為了要驗證這樣的情況,特別設計一個主控台應用程式的專案,其程式碼如下

namespace Background_Service_New_Thread_Await
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(async () =>
            {
                Thread.CurrentThread.Name = "新 new Thread 的處理服務用執行緒(非來自執行緒集區)";
                Console.WriteLine($"開始進行背景服務程式執行");
                // 模擬這個背景服務要處理的同步程式碼執行動作
                Thread.Sleep(2000);
                Console.WriteLine($"準備進行非同步 await 呼叫");
                Console.WriteLine();

                ShowThreadInformation("呼叫 await 前,new Thread 的相關資訊");

                // 模擬要使用 await 來進行非同步呼叫
                await Task.Delay(3000);

                ShowThreadInformation("呼叫 await 後,new Thread 的相關資訊");
            });
            thread.Start();

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

        // 將當前的執行緒資訊顯示出來
        static void ShowThreadInformation(string message)
        {
            Console.WriteLine(message);
            Console.WriteLine($"執行緒 Id : {Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"執行緒名稱 : {Thread.CurrentThread.Name}");
            Console.WriteLine($"來自集區 : {Thread.CurrentThread.IsThreadPoolThread}");
            Console.WriteLine($"為背景執行緒 : {Thread.CurrentThread.IsBackground}");
            Console.WriteLine();
        }
    }
}

在上面的程式碼,設計的一個 [ShowThreadInformation] 方法,這個方法會將當前執行緒的 Id , 名稱 Name , 是否該執行緒來自於執行緒集區 、 該執行緒是否為背景執行緒 資訊顯示在螢幕上。

在主執行緒內,首先使用 new Thread 運算式來建立一個執行緒物件,在該執行緒所綁定的委派方法內,將會使用 await Task.Delay(3000); 敘述來執行 await 的工作,並且在這個 Lambda 委派方法上,使用了 async 修飾詞 async ()=> {...} 標明這個委派方法,是一個非同步方法。

而在執行 await 運算式的前後,將會呼叫 [ShowThreadInformation] 方法,顯示當前執行緒的資訊。

現在可以執行這個專案,將會看到底下的執行內容

Press any key for continuing...
開始進行背景服務程式執行
準備進行非同步 await 呼叫

呼叫 await 前,new Thread 的相關資訊
執行緒 Id : 10
執行緒名稱 : 新 new Thread 的處理服務用執行緒(非來自執行緒集區)
來自集區 : False
為背景執行緒 : False

呼叫 await 後,new Thread 的相關資訊
執行緒 Id : 6
執行緒名稱 : .NET ThreadPool Worker
來自集區 : True
為背景執行緒 : True

從執行結果可以看的出來,在呼叫 await 之前,當前的執行緒卻是為前景執行緒,並且不是來自於執行緒集區內,另外,在該執行緒執行後,就會執行 Thread.CurrentThread.Name = "新 new Thread 的處理服務用執行緒(非來自執行緒集區)"; 敘述,設定這個執行緒 Name 的屬性值,這樣的設定結果,可以從執行結果看的出來。

當 await 執行完成後,再度顯示當前執行緒的狀態,發現到現在的執行緒 ID 已經改變了,名稱也不對了,最重要的是這個執行緒來自於執行緒集區,而且是個背景執行緒。 




2022年7月27日 星期三

C# 非同步 : Task.Run 要在哪裡傳入與檢查 CancellationToken

Task.Run 要在哪裡傳入與檢查 CancellationToken

這篇文章將會說明,對於一開始學習 Task.Run 方法與想要透過 CancellationToken 取消權杖來取消工作的開發者,將會有個迷失點,這個問題在於,當查看 Task.Run 方法 文件的時候,將會看到 Run(Action, CancellationToken) 這個函式簽章,而這個方法將透過執行緒集區佇列內進行排隊,取得一個可用執行緒,並且將 Action 這個委派方法交由這個執行緒來執行,並傳回代表該工作的 Task 物件;這個方法的第二個參數,則是一個 [CancellationToken] 型別的參數,代表一個取消權杖可用來在工作尚未開始之前取消工作。

若沒有看到上面最後一段說明,絕大部分的開發者都會以為,只要在呼叫 Task.Run 方法的時候,將取消權杖傳入進去,在任何時候就可以透過產生該取消權證的 CancellationTokenSource 這個型別的物件,發出呼叫 CancellationTokenSource.Cancel 方法 , 送出與傳遞取消要求,此時,這個 Task.Run 這個非同步工作,就會取消執行了。

從上面的說明可以看出不是這樣的運作的,當呼叫 Task.Run 方法所傳入的取消權杖,僅會在要呼叫 Task.Run 方法前,這個取消權杖就已將發出取消請求了,所以,一旦呼叫了 Task.Run 的方法,就不會去取得一個非同步的執行緒,並且開始執行所指定的委派方法,如此,可以節省系統資源的使用。

現在來測試這樣的說明,首先,建立一個 主控台應用程式,其程式碼如下:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    cts.Cancel();
    Console.WriteLine($"1 建立非同步工作");
    var task = Task.Run(() =>
    {
        Console.WriteLine($"  2 非同步工作已經開始執行");
        Thread.Sleep(3000);
        Console.WriteLine($"  3 非同步工作已經結束執行");
    }, token);
    Console.WriteLine($"4 主執行緒休息 2 秒");
    Thread.Sleep(2000);
    Console.WriteLine($"5 主執行緒即將結束執行");
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}

這是一個非常經典的取消權杖的程式設計準則,在程式要開始執行之前,首先建立一個 CancellationTokenSource 物件,這個物件可用於取得取消權杖與可以發出取消請求的方法,接著,將取消權杖透過 CancellationToken token = cts.Token 敘述來取得,有了取消權杖,之後面可以將取消權杖傳遞到其他的非同步方法或者非同工作內。

在這個時候,將會呼叫 cts.Cancel() 方法,對於該取消權杖發出取消工作的請求,此時,該取消權杖處於已經取消的狀態下,接著,透過 Task.Run 方法建立一個非同步工作,這個非同步工作所指定的委派方法將會休息三秒鐘後,就會結束執行。

同時,在主執行緒端,一旦非同步工作建立與開始執行後,將會休息 2 秒鐘

現在,執行這個專案,將會得到底下的執行結果

1 建立非同步工作
4 主執行緒休息 2 秒
5 主執行緒即將結束執行
Press any key for continuing...

從執行結果的輸出內容可以看出,這個非同步工作物件所指定的委派方法,是沒有被執行的,也就是說,雖然呼叫了 Task.Run 方法,系統並沒有從執行緒集區內取得一個執行緒來執行指定的委派方法程式碼。

現在將測試程式碼修改成為如下:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    Console.WriteLine($"1 建立非同步工作");
    var task = Task.Run(() =>
    {
        Console.WriteLine($"  2 非同步工作已經開始執行");
        Thread.Sleep(3000);
        Console.WriteLine($"  3 非同步工作已經結束執行");
    }, token);
    Console.WriteLine($"4 主執行緒休息 1 秒");
    cts.CancelAfter(2000);
    Thread.Sleep(1000);
    Console.WriteLine($"5 主執行緒即將結束執行");
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}

前半段幾乎沒有變動,除了將一開始就發出取消權杖的呼叫,將其程式碼移除了。

在非同步工作建立之後,使用了 cts.CancelAfter(2000); 敘述,將會於 2 秒鐘之後,對取消權杖發出取消的請求通知,這個敘述執行完後,並不會暫停2秒鐘,而會繼續往下執行,現在,在主執行緒內,將會休息一秒鐘後,準備要結束執行。

底下是執行這個專案的螢幕輸出結果

1 建立非同步工作
4 主執行緒休息 1 秒
  2 非同步工作已經開始執行
5 主執行緒即將結束執行
Press any key for continuing...
  3 非同步工作已經結束執行

從執行結果可以看出,雖然在呼叫 Task.Run 方法的時候,有傳入取消權杖,不過,請求取消的動作,是在呼叫 Task.Run 之後,因此,在 Task.Run 所傳入的取消權杖是沒有效用的,結論就是,想要建立一個非同步工作,讓這個非同步工作具有取消權杖的效果,建議如下:

  • 在 Task.Run 方法內,傳入取消權杖
  • 在 Task.Run 的委派方法內,也要輪詢檢查取消權杖是否已經發出取消請求

接著,將測試程式碼修改如下:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    Console.WriteLine($"1 建立非同步工作");
    var task = Task.Run(() =>
    {
        Console.WriteLine($"  2 非同步工作已經開始執行");
        token.ThrowIfCancellationRequested();
        Thread.Sleep(3000);
        token.ThrowIfCancellationRequested();
        Console.WriteLine($"  3 非同步工作已經結束執行");
    }, token);
    Console.WriteLine($"4 主執行緒休息 1 秒");
    cts.CancelAfter(2000);
    Thread.Sleep(1000);
    Console.WriteLine($"5 主執行緒即將結束執行");
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}

在這裡將會在非同步工作內的委派方法中,使用輪詢 Polling 方式,檢查取消權杖的狀態,這裡使用了 token.ThrowIfCancellationRequested(); 敘述,一旦有取消請求發出,則將會拋出例外異常,結束這個非同步工作的執行。

底下是執行結果

1 建立非同步工作
4 主執行緒休息 1 秒
  2 非同步工作已經開始執行
5 主執行緒即將結束執行
Press any key for continuing...

從執行結果可以看出,當發出取消請求之後,這個取消權杖被設定為取消狀態,而這個非同的工作也就拋出例外異常,而中止了。

最後,再來將程式碼改成底下內容:

static async Task Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    Console.WriteLine($"1 建立非同步工作 {DateTime.Now.TimeOfDay}");
    var task = Task.Run(async () =>
    {
        Console.WriteLine($"  2 非同步工作已經開始執行 {DateTime.Now.TimeOfDay}");
        token.ThrowIfCancellationRequested();
        await Task.Delay(3000, token);
        token.ThrowIfCancellationRequested();
        Console.WriteLine($"  3 非同步工作已經結束執行 {DateTime.Now.TimeOfDay}");
    }, token);
    Console.WriteLine($"4 主執行緒休息 1 秒 {DateTime.Now.TimeOfDay}");
    cts.CancelAfter(2000);
    try
    {
        await task;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"5 非同步工作取消了 {ex.GetType().ToString()} : {DateTime.Now.TimeOfDay}");
    }
    Console.WriteLine($"6 主執行緒即將結束執行 {DateTime.Now.TimeOfDay}");
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}

在上面的程式碼,對 Console.WriteLine 方法內,將會加入當時呼叫的時間點內容,這裡是要來判斷在非同步工作內委派方法,在等待 3 秒鐘的時候,是否不會在 3 秒內的任何時間點,若接收到取消通知,都會立即中止等候,直接結束這個非同步工作。

對於在非同步工作內的委派方法,原先使用 Thread.Sleep(3000) 的封鎖等待的呼叫,將會改成 await Task.Delay(3000, token); 非封鎖式的呼叫,並且在 Delay 方法的後面,傳入取消權杖,要 Task.Delay 這個方法要能夠關注取消權杖是否發出請求。

在主執行緒端,也將休息2秒的敘述,改成 await task; 這個敘述,並且將其包裹在 try catch 敘述內,因為,一旦這個非同步工作有取消動作產生,將會拋出例外異常,所以,將會在此捕捉這個例外異常,查看非同步工作內的委派方法,何時觸發了取消動作。

現在,可以來執行這個專案,得到底下的執行結果

1 建立非同步工作 16:22:37.3960409
4 主執行緒休息 1 秒 16:22:37.4027808
  2 非同步工作已經開始執行 16:22:37.4033487
5 非同步工作取消了 System.Threading.Tasks.TaskCanceledException : 16:22:39.4463615
6 主執行緒即將結束執行 16:22:39.4464650
Press any key for continuing...

從執行結果可以看出,非同步工作內的委派方法於 37 秒的時候開始執行,接著要休息 3 秒鐘,因此,這個非同步工作應該會於 40 秒的時候結束執行。

不過,在主執行緒端,將會於 2 秒鐘之後發出取消請求,因此,這個非同步工作將會於 39 秒的時候拋出例外異常,這裡看到的例外異常是 TaskCanceledException ,這代表表示用來傳達工作取消的例外狀況;從捕捉到例外異常的時間點,也正好是 39 秒,符合這個程式的設計。

結論

當進行非同步工作設計的時候,若有取消請求的設計需求,還是需要在非同步工作內的委派方法裡面,輪詢取消權杖,適時做出處置,這點功夫是不能缺少的。