天天看點

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

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

目錄

TaskAwaiter

延續的另一種方法

另一種建立任務的方法

實作一個支援同步和異步任務的類型

Task.FromCanceled()

如何在内部取消任務

Yield 關鍵字

補充知識點

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

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

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

Task 有個 GetAwaiter() 方法,會傳回TaskAwaiter 或TaskAwaiter,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.ConfiguredTaskAwaiter 擁有跟 TaskAwaiter 一樣的屬性和方法。

.ContinueWith() 跟 .ConfigureAwait(false) 還有一個差別就是 前者可以延續多個任務和延續任務的任務(多層)。後者隻能延續一層任務(一層可以有多個任務)。

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

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

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 類可以對任務的生命周期做控制。

首先要通過 .Task 屬性,獲得一個 Task 或 Task 。

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 繼續進行講解。

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

使用者可以使用 GetResult() 同步擷取結果;

使用者可以使用 RunAsync() 執行任務,使用 .Result 屬性異步擷取結果;

其實作如下:

///

/// 實作同步任務和異步任務的類型

public class MyTaskClass

{

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,它因指定的取消标記進行的取消操作而完成。

這裡筆者抄來了一個示例:

var token = new CancellationToken(true);

Task task = Task.FromCanceled(token);

Task genericTask = Task.FromCanceled(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 就能取消任務。

我們可以使用 CancellationToken 的 ThrowIfCancellationRequested() 方法抛出 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 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 才行嘛?那些文檔說到什麼疊代器接口什麼的,又是什麼東西呢?

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

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 方法。

其實 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 ,而不需要你先實作 IEnumerable 。

線程同步有多種方法:臨界區(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 篇,後面會繼續深入多線程和任務的更多使用方法和場景。

原文位址

https://www.cnblogs.com/whuanle/p/12802943.html

繼續閱讀