C#多線程(15):任務基礎③


任務基礎一共三篇,本篇是第三篇,之后開始學習異步編程、并發、異步I/O的知識。

本篇會繼續講述 Task 的一些 API 和常用的操作。

TaskAwaiter

先說一下 TaskAwaiter,TaskAwaiter 表示等待異步任務完成的對象并為結果提供參數。

Task 有個 GetAwaiter() 方法,會返回TaskAwaiterTaskAwaiter<TResult>,TaskAwaiter 類型在 System.Runtime.CompilerServices 命名空間中定義。

TaskAwaiter 類型的屬性和方法如下:

屬性:

屬性 說明
IsCompleted 獲取一個值,該值指示異步任務是否已完成。

方法:

方法 說明
GetResult() 結束異步任務完成的等待。
OnCompleted(Action) 將操作設置為當 TaskAwaiter 對象停止等待異步任務完成時執行。
UnsafeOnCompleted(Action) 計劃與此 awaiter 相關異步任務的延續操作。

使用示例如下:

        static void Main()
        {
            Task<int> task = new Task<int>(()=>
            {
                Console.WriteLine("我是前驅任務");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            TaskAwaiter<int> awaiter = task.GetAwaiter();

            awaiter.OnCompleted(()=>
            {
                Console.WriteLine("前驅任務完成時,我就會繼續執行");
            });
            task.Start();

            Console.ReadKey();
        }

另外,我們前面提到過,任務發生未經處理的異常,任務被終止,也算完成任務。

延續的另一種方法

上一節我們介紹了 .ContinueWith() 方法來實現延續,這里我們介紹另一個延續方法 .ConfigureAwait()。

.ConfigureAwait() 如果要嘗試將延續任務封送回原始上下文,則為 true;否則為 false。

我來解釋一下, .ContinueWith() 延續的任務,當前驅任務完成后,延續任務會繼續在此線程上繼續執行。這種方式是同步的,前者和后者連續在一個線程上運行。

.ConfigureAwait(false) 方法可以實現異步,前驅方法完成后,可以不理會后續任務,而且后續任務可以在任意一個線程上運行。這個特性在 UI 界面程序上特別有用。

可以參考:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f

其使用方法如下:

        static void Main()
        {
            Task<int> task = new Task<int>(()=>
            {
                Console.WriteLine("我是前驅任務");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });

            ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter();

            awaiter.OnCompleted(()=>
            {
                Console.WriteLine("前驅任務完成時,我就會繼續執行");
            });
            task.Start();

            Console.ReadKey();
        }

ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter 擁有跟 TaskAwaiter 一樣的屬性和方法。

.ContinueWith() .ConfigureAwait(false) 還有一個區別就是 前者可以延續多個任務和延續任務的任務(多層)。后者只能延續一層任務(一層可以有多個任務)。

另一種創建任務的方法

前面提到提到過,創建任務的三種方法:new Task()、Task.Run()、Task.Factory.SatrtNew(),現在來學習第四種方法:TaskCompletionSource<TResult> 類型。

我們來看看 TaskCompletionSource<TResulr> 類型的屬性和方法:

屬性:

屬性 說明
Task 獲取由此 Task 創建的 TaskCompletionSource。

方法:

方法 說明
SetCanceled() 將基礎 Task 轉換為 Canceled 狀態。
SetException(Exception) 將基礎 Task 轉換為 Faulted 狀態,并將其綁定到一個指定異常上。
SetException(IEnumerable) 將基礎 Task 轉換為 Faulted 狀態,并對其綁定一些異常對象。
SetResult(TResult) 將基礎 Task 轉換為 RanToCompletion 狀態。
TrySetCanceled() 嘗試將基礎 Task 轉換為 Canceled 狀態。
TrySetCanceled(CancellationToken) 嘗試將基礎 Task 轉換為 Canceled 狀態并啟用要存儲在取消的任務中的取消標記。
TrySetException(Exception) 嘗試將基礎 Task 轉換為 Faulted 狀態,并將其綁定到一個指定異常上。
TrySetException(IEnumerable) 嘗試將基礎 Task 轉換為 Faulted 狀態,并對其綁定一些異常對象。
TrySetResult(TResult) 嘗試將基礎 Task 轉換為 RanToCompletion 狀態。

TaskCompletionSource<TResulr> 類可以對任務的生命周期做控制。

首先要通過 .Task 屬性,獲得一個 TaskTask<TResult> 。

            TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task;	//  Task myTask = task.Task;

然后通過 task.xxx() 方法來控制 myTask 的生命周期,但是呢,myTask 本身是沒有任務內容的。

使用示例如下:

        static void Main()
        {
            TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task;       // task 控制 myTask

            // 新開一個任務做實驗
            Task mainTask = new Task(() =>
            {
                Console.WriteLine("我可以控制 myTask 任務");
                Console.WriteLine("按下任意鍵,我讓 myTask 任務立即完成");
                Console.ReadKey();
                task.SetResult(666);
            });
            mainTask.Start();

            Console.WriteLine("開始等待 myTask 返回結果");
            Console.WriteLine(myTask.Result);
            Console.WriteLine("結束");
            Console.ReadKey();
        }

其它例如 SetException(Exception) 等方法,可以自行探索,這里就不再贅述。

參考資料:https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/

這篇文章講得不錯,而且有圖:https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/

實現一個支持同步和異步任務的類型

這部分內容對 TaskCompletionSource<TResult> 繼續進行講解。

這里我們來設計一個類似 Task 類型的類,支持同步和異步任務。

  • 用戶可以使用 GetResult() 同步獲取結果;
  • 用戶可以使用 RunAsync() 執行任務,使用 .Result 屬性異步獲取結果;

其實現如下:

/// <summary>
/// 實現同步任務和異步任務的類型
/// </summary>
/// <typeparam name="TResult"></typeparam>
public class MyTaskClass<TResult>
{
    private readonly TaskCompletionSource<TResult> source = new TaskCompletionSource<TResult>();
    private Task<TResult> task;
    // 保存用戶需要執行的任務
    private Func<TResult> _func;

    // 是否已經執行完成,同步或異步執行都行
    private bool isCompleted = false;
    // 任務執行結果
    private TResult _result;

    /// <summary>
    /// 獲取執行結果
    /// </summary>
    public TResult Result
    {
        get
        {
            if (isCompleted)
                return _result;
            else return task.Result;
        }
    }
    public MyTaskClass(Func<TResult> func)
    {
        _func = func;
        task = source.Task;
    }

    /// <summary>
    /// 同步方法獲取結果
    /// </summary>
    /// <returns></returns>
    public TResult GetResult()
    {
        _result = _func.Invoke();
        isCompleted = true;
        return _result;
    }

    /// <summary>
    /// 異步執行任務
    /// </summary>
    public void RunAsync()
    {
        Task.Factory.StartNew(() =>
        {
            source.SetResult(_func.Invoke());
            isCompleted = true;
        });
    }
}

我們在 Main 方法中,創建任務示例:

    class Program
    {
        static void Main()
        {
            // 實例化任務類
            MyTaskClass<string> myTask1 = new MyTaskClass<string>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });

            // 直接同步獲取結果
            Console.WriteLine(myTask1.GetResult());


            // 實例化任務類
            MyTaskClass<string> myTask2 = new MyTaskClass<string>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });

            // 異步獲取結果
            myTask2.RunAsync();

            Console.WriteLine(myTask2.Result);


            Console.ReadKey();
        }
    }

Task.FromCanceled()

微軟文檔解釋:創建 Task,它因指定的取消標記進行的取消操作而完成。

這里筆者抄來了一個示例

var token = new CancellationToken(true);
Task task = Task.FromCanceled(token);
Task<int> genericTask = Task.FromCanceled<int>(token);

網上很多這樣的示例,但是,這個東西到底用來干嘛的?new 就行了?

帶著疑問我們來探究一下,來個示例:

        public static Task Test()
        {
            CancellationTokenSource source = new CancellationTokenSource();
            source.Cancel();
            return Task.FromCanceled<object>(source.Token);
        }
        static void Main()
        {
            var t = Test();	// 在此設置斷點,監控變量
            Console.WriteLine(t.IsCanceled);
         }

Task.FromCanceled() 可以構造一個被取消的任務。我找了很久,沒有找到很好的示例,如果一個任務在開始前就被取消,那么使用 Task.FromCanceled() 是很不錯的。

這里有很多示例可以參考:https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/

如何在內部取消任務

之前我們討論過,使用 CancellationToken 取消令牌傳遞參數,使任務取消。但是都是從外部傳遞的,這里來實現無需 CancellationToken 就能取消任務。

我們可以使用 CancellationTokenThrowIfCancellationRequested() 方法拋出 System.OperationCanceledException 異常,然后終止任務,任務會變成取消狀態,不過任務需要先傳入一個令牌。

這里筆者來設計一個難一點的東西,一個可以按順序執行多個任務的類。

示例如下:

    /// <summary>
    /// 能夠完成多個任務的異步類型
    /// </summary>
    public class MyTaskClass
    {
        private List<Action> _actions = new List<Action>();
        private CancellationTokenSource _source = new CancellationTokenSource();
        private CancellationTokenSource _sourceBak = new CancellationTokenSource();
        private Task _task;

        /// <summary>
        ///  添加一個任務
        /// </summary>
        /// <param name="action"></param>
        public void AddTask(Action action)
        {
            _actions.Add(action);
        }

        /// <summary>
        /// 開始執行任務
        /// </summary>
        /// <returns></returns>
        public Task StartAsync()
        {
            // _ = new Task() 對本示例無效
            _task = Task.Factory.StartNew(() =>
             {
                 for (int i = 0; i < _actions.Count; i++)
                 {
                     int tmp = i;
                     Console.WriteLine($"第 {tmp} 個任務");
                     if (_source.Token.IsCancellationRequested)
                     {
                         Console.ForegroundColor = ConsoleColor.Red;
                         Console.WriteLine("任務已經被取消");
                         Console.ForegroundColor = ConsoleColor.White;
                         _sourceBak.Cancel();
                         _sourceBak.Token.ThrowIfCancellationRequested();
                     }
                     _actions[tmp].Invoke();
                 }
             },_sourceBak.Token);
            return _task;
        }

        /// <summary>
        /// 取消任務
        /// </summary>
        /// <returns></returns>
        public Task Cancel()
        {
            _source.Cancel();

            // 這里可以省去
            _task = Task.FromCanceled<object>(_source.Token);
            return _task;
        }
    }

Main 方法中:

        static void Main()
        {
            // 實例化任務類
            MyTaskClass myTask = new MyTaskClass();

            for (int i = 0; i < 10; i++)
            {
                int tmp = i;
                myTask.AddTask(() =>
                {
                    Console.WriteLine("     任務 1 Start");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine("     任務 1 End");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                });
            }

            // 相當于 Task.WhenAll()
            Task task = myTask.StartAsync();
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine($"任務是否被取消:{task.IsCanceled}");

            // 取消任務
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("按下任意鍵可以取消任務");
            Console.ForegroundColor = ConsoleColor.White;
            Console.ReadKey();

            var t = myTask.Cancel();    // 取消任務
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine($"任務是否被取消:【{task.IsCanceled}】");

            Console.ReadKey();
        }

你可以在任一階段取消任務。

Yield 關鍵字

迭代器關鍵字,使得數據不需要一次性返回,可以在需要的時候一條條迭代,這個也相當于異步。

迭代器方法運行到 yield return 語句時,會返回一個 expression,并保留當前在代碼中的位置。 下次調用迭代器函數時,將從該位置重新開始執行。

可以使用 yield break 語句來終止迭代。

官方文檔:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield

網上的示例大多數都是 foreach 的,有些同學不理解這個到底是啥意思。筆者這里簡單說明一下。

我們也可以這樣寫一個示例:

這里已經沒有 foreach 了。

        private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                i++;
                yield return list[i];
            }
        }

但是,同學又問,這個 return 返回的對象 要實現這個 IEnumerable<T> 才行嘛?那些文檔說到什么迭代器接口什么的,又是什么東西呢?

我們可以先來改一下示例:

        private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                int num = list[i];
                i++;
                yield return num;
            }
        }

你在 Main 方法中調用,看看是不是正常運行?

        static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

這樣說明了,yield return 返回的對象,并不需要實現 IEnumerable<int> 方法。

其實 yield 是語法糖關鍵字,你只要在循環中調用它就行了。

        static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }

        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < 100)
            {
                i++;
                yield return i;
            }
        }
    }

它會自動生成 IEnumerable<T> ,而不需要你先實現 IEnumerable<T> 。

補充知識點

  • 線程同步有多種方法:臨界區(Critical Section)、互斥量(Mutex)、信號量(Semaphores)、事件(Event)、任務(Task);

  • Task.Run()Task.Factory.StartNew() 封裝了 Task;

  • Task.Run()Task.Factory.StartNew() 的簡化形式;

  • 有些地方 net Task() 是無效的;但是 Task.Run()Task.Factory.StartNew() 可以;

本篇是任務基礎的終結篇,至此 C# 多線程系列,一共完成了 15 篇,后面會繼續深入多線程和任務的更多使用方法和場景。

喜歡我的作者記得關注我喲~

posted @ 2020-04-29 16:20  癡者工良  閱讀(...)  評論(...編輯  收藏
美人江湖手游