C#中CancellationToken和CancellationTokenSource用法


  之前做開發時,一直沒注意這個東西,做了.net core之后,發現CancellationToken用的越來越平凡了。

  這也難怪,原來.net framework使用異步的不是很多,而.net core首推異步編程,到處可以看到Task的影子,而CancellationToken正好是異步Task的一個控制器!所以花點時間做個筆記

  

  CancellationToken

  CancellationToken有一個構造函數,可以傳入一個bool類型表示當前的CancellationToken是否是取消狀態。另外,因為CancellationToken是一個結構體,所以它還有一個空參數的構造函數。  

    public CancellationToken();//因為是結構體,才有空構造函數,不過沒什么作用
    public CancellationToken(bool canceled);

  屬性如下:  

    //靜態屬性,獲取一個空的CancellationToken,這個CancellationToken注冊的回調方法不會被觸發,作用類似於使用空構造函數得到的CancellationToken
    public static CancellationToken None { get; }
    //表示當前CancellationToken是否可以被取消
    public bool CanBeCanceled { get; }
    //表示當前CancellationToken是否已經是取消狀態
    public bool IsCancellationRequested { get; }
    //和CancellationToken關聯的WaitHandle對象,CancellationToken注冊的回調方法執行時通過這個WaitHandle實現的
    public WaitHandle WaitHandle { get; }

  常用方法:  

    //往CancellationToken中注冊回調
    public CancellationTokenRegistration Register(Action callback);
    public CancellationTokenRegistration Register(Action callback, bool useSynchronizationContext);
    public CancellationTokenRegistration Register([NullableAttribute(new[] { 1, 2 })] Action<object?> callback, object? state);
    public CancellationTokenRegistration Register([NullableAttribute(new[] { 1, 2 })] Action<object?> callback, object? state, bool useSynchronizationContext);
    //當CancellationToken處於取消狀態是,拋出System.OperationCanceledException異常
    public void ThrowIfCancellationRequested();

  常用的注冊回調的方法是上面4個Register方法,其中callback是回調執行的委托,useSynchronizationContext表示是否使用同步上下文,state是往回調委托中傳的參數值

  另外,Register方法會返回一個CancellationTokenRegistration結構體,當注冊回調之后,可以調用CancellationTokenRegistration的Unregister方法來取消注冊,這個Unregister方法會返回一個bool值,當成功取消時返回true,當取消失敗(比如回調已執行)將返回false:  

    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    var cancellationTokenRegistration = cancellationTokenSource.Token.Register(() =>
    {
        Console.WriteLine("Canceled");//這里將不會執行輸出
    });

    //cancellationTokenSource.Cancel();
    //var result = cancellationTokenRegistration.Unregister();//result = false

    var result = cancellationTokenRegistration.Unregister();//result = true
   cancellationTokenSource.Cancel();

   上面提到,CancellationToken可以使用構造函數直接構造,同時可以傳入一個參數,表示當前的狀態,需要注意的是,CancellationToken的狀態最多可以改變一次,也就是從未取消變成已取消。

  如果構造時傳入true,也就是說CancellationToken是已取消狀態,這個時候注冊的回調都會立即執行:  

    CancellationToken cancellationToken = new CancellationToken(true);
    cancellationToken.Register(() =>
    {
        Console.WriteLine("Canceled");//這里會立即執行輸出Canceled
    });

  但如果構造時傳入的是false,說明CancellationToken處於未取消狀態,這時候注冊的回到都會處於一個待觸發狀態:  

    CancellationToken cancellationToken = new CancellationToken(false);
    cancellationToken.Register(() =>
    {
        Console.WriteLine("Canceled");//這里不會立即執行輸出
    });

  通過Register方法注冊的服務只會執行一次!

  但一般的,如果傳入false構造出來的CancellationToken,可以認為是不會觸發的,因為它沒有觸發的方法!所以一般的,我們都不會直接使用構造函數創建CancellationToken,而是使用CancellationTokenSource對象來獲取一個CancellationToken

 

  CancellationTokenSource

  CancellationTokenSource可以理解為CancellationToken的控制器,控制它什么時候變成取消狀態的一個對象,它有一個CancellationToken類型的屬性Token,只要CancellationTokenSource創建,這個Token也會被創建,同時Token會和這個CancellationTokenSource綁定:  

    //表示Token是否已處於取消狀態
    public bool IsCancellationRequested { get; }
    //CancellationToken 對象
    public CancellationToken Token { get; }

  可以直接創建一個CancellationTokenSource對象,同時指定一個時間段,當過了這段時間后,CancellationTokenSource就會自動取消了。

  CancellationTokenSource的取消有4個方法:  

    //立刻取消
    public void Cancel();
    //立刻取消
    public void Cancel(bool throwOnFirstException);
    //延遲指定時間后取消
    public void CancelAfter(int millisecondsDelay);
    //延遲指定時間后取消
    public void CancelAfter(TimeSpan delay);

  Cancel和兩個CancelAfter方法沒什么特別的,主要就是有一個延遲的效果,需要注意的是Cancel的兩個重載之間的區別。

  首先,上面說道,CancellationToken狀態只能改變一次(從未取消變成已取消),當CancellationToken時已取消狀態時,每次往其中注冊的回調都會立刻執行!當處於未取消狀態時,注冊進去的回調都會等待執行。

  需要注意的是,當在未取消狀態下注冊多個回調時,它們在執行時是一個類似棧的結構順序,先注冊后執行。

  而CancellationToken的Register可以注冊多個回調,那他們可能都會拋出異常,throwOnFirstException參數表示在第一次報錯時的處理行為.

  throwOnFirstException = true 表示立即拋出當前發生的異常,后續的回調將會取消執行  

    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    try
    {
        cancellationTokenSource.Token.Register(() =>
        {
            throw new Exception("1");
        });
        cancellationTokenSource.Token.Register(() =>
        {
            throw new Exception("2");//不會執行
        });

        cancellationTokenSource.Cancel(true);
    }
    catch (Exception ex)
    {
        //ex is System.Exception("1")
    }

   throwOnFirstException = false 表示跳過當前回調的異常,繼續執行生效的回調,等所有的回調執行完成之后,再將所有的異常打包成一個System.AggregateException異常拋出來!  

    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    try
    {
        cancellationTokenSource.Token.Register(() =>
        {
            throw new Exception("1");
        });
        cancellationTokenSource.Token.Register(() =>
        {
            throw new Exception("2");
        });

        cancellationTokenSource.Cancel(false);//相當於cancellationTokenSource.Cancel()
    }
    catch (Exception ex)
    {
        //ex is System.AggregateException:[Exception("2"),Exception("1")]
    }

   CancellationTokenSource還可以與其它CancellationToken關聯起來,生成一個新的CancellationToken,當其他CancellationToken取消時,會自動觸發當前的CancellationTokenSource執行取消動作!  

    CancellationTokenSource cancellationTokenSource1 = new CancellationTokenSource();
    cancellationTokenSource1.Token.Register(() =>
    {
        Console.WriteLine("Cancel1");
    });
    CancellationTokenSource cancellationTokenSource2 = new CancellationTokenSource();
    cancellationTokenSource2.Token.Register(() =>
    {
        Console.WriteLine("Cancel2");
    });
    CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource1.Token, cancellationTokenSource2.Token);
    cancellationTokenSource.Token.Register(() =>
    {
        Console.WriteLine("Cancel");
    });

    //cancellationTokenSource1.Cancel(); //執行這個依次輸出 Cancel    Cancel1
    cancellationTokenSource2.Cancel(); //執行這個依次輸出 Cancel    Cancel2

 

  使用場景一

  當我們創建異步操作時,可以傳入一個CancellationToken,當異步操作處於等待執行狀態時,可以通過設置CancellationToken為取消狀態將異步操作取消執行:  

    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    var task = new Task(() =>
    {
        Thread.Sleep(1500);//執行了2秒中代碼 Console.WriteLine(
"Execute Some Code"); }, cancellationTokenSource.Token); task.Start();//啟動,等待調度執行 //發現不對,可以取消task執行 cancellationTokenSource.Cancel(); Thread.Sleep(1000);//等待1秒 Console.WriteLine("Task狀態:" + task.Status);//Canceled

   但是經常的,我們的取消動作可能不會那么及時,如果異步已經執行了,再執行取消時無效的,這是就需要我們自己在異步委托中檢測了:  

    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    var task = new Task(() =>
    {
        Thread.Sleep(1500);//執行了2秒中代碼
        cancellationTokenSource.Token.ThrowIfCancellationRequested();
        Console.WriteLine("Execute Some Code");
    }, cancellationTokenSource.Token);

    task.Start();//啟動,等待調度執行

    Thread.Sleep(1000);////一段時間后發現不對,可以取消task執行
    cancellationTokenSource.Cancel();
    Thread.Sleep(1000);//等待1秒
    Console.WriteLine("Task狀態:" + task.Status);//Canceled

  

  使用場景二

   有時,我們希望在觸發某個時間后,可以執行某些代碼功能,但是在異步環境下,我們不能保證那些要執行的代碼是否已准備好了,比如我們有一個Close方法,當調用Close后表示是關閉狀態,如果我們相當程序處於關閉狀態時執行一些通知,一般的,我們可能是想到采用事件模型,或者在Close方法傳入事件委托,或者采用一些諸如模板設計這樣的模型去實現:   

    class Demo
    {
        public void Close(Action callback)
        {
            //關閉
            Thread.Sleep(3000);

            callback?.Invoke();//執行通知
        }
    }

  或者  

    class Demo
    {
        public event Action Callback;

        public void Close()
        {
            //關閉
            Thread.Sleep(3000);

            Callback?.Invoke();//執行通知
        }
    }

  但是這就有問題了,如果是傳入參數或者采用事件模型,因為前面說過了,如果在異步環境下,我們不能保證那些要執行的代碼是否已准備好了,也許在執行Close方法時,程序還未注冊回調。

  這個時候就可以使用CancellationToken來解決這個問題:  

    class Demo
    {
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

        public CancellationToken Token { get => cancellationTokenSource.Token; }

        public void Close()
        {
            //關閉
            Thread.Sleep(3000);

            cancellationTokenSource.Cancel();//執行通知
        }
    }

  主需要往Token屬性中注冊回調而無需關注Close什么時候執行了

 

  使用場景三

  有時候,我們寫一個異步無限循環的方法去處理一些問題,而我們希望可以在方法外來停止它這個時候,我們就可以通過返回CancellationTokenSource來實現了:  

        public CancellationTokenSource Listen()
        {
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

            //循環調度執行
            Task.Run(() =>
            {
                while (true)
                {
                    cancellationTokenSource.Token.ThrowIfCancellationRequested();

                    //循環執行一些操作
                    Thread.Sleep(1000);
                    Console.WriteLine("Run"); } });
return cancellationTokenSource; }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM