C#多執行緒(15):任務基礎③

痴者工良發表於2020-04-29


任務基礎一共三篇,本篇是第三篇,之後開始學習非同步程式設計、併發、非同步I/O的知識。

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

TaskAwaiter

先說一下 TaskAwaiterTaskAwaiter 表示等待非同步任務完成的物件併為結果提供引數。

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 篇,後面會繼續深入多執行緒和任務的更多使用方法和場景。

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

相關文章