異步編程基礎

>>返回《C# 併發編程》
html

1. 概述

前面的文章介紹了標識了 asyncawait 的代碼,是怎麼被線程執行的。編程

>>同步上下文-7.5 異步編程(Async)併發

下面介紹一些類庫和經常使用的API異步

2. 報告進度

使用 IProgress<T>Progress<T> 類型async

  1. 構造 Progress<T> 實例時捕獲當前 同步上下文 實例;
  2. Progress<T> 實例的ProgressChanged 事件被調用時使用上面捕獲的同步上下文
  3. 若是在執行構造函數的線程沒有同步上下文時(隱含使用的Default同步上下文),則將在 ThreadPool 中調用事件
static async Task DoProgressAsync(int count, IProgress<int> progress = null)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(200);
        if (progress != null)
            progress.Report(i + 1);
    }
}

static async Task CallProgressAsync()
{
    int count = 5;
    var progress = new Progress<int>();
    progress.ProgressChanged += (sender, args) =>
    {
        System.Console.WriteLine($"{args}/{count}");
    };
    await DoProgressAsync(count, progress);
}

輸出爲:異步編程

1/5
2/5
3/5
4/5
5/5

3. 等待一組任務完成

Task.WhenAll 方法有以 IEnumerable 類型做爲參數的重載,但建議你們不要使用。函數

  • 調用 ToListToArray 方法後,序列中沒有啓動的任務就開始了
static async Task DownloadAllAsync()
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    IEnumerable<string> urls = new string[]{
                "https://www.baidu.com/",
                "https://cn.bing.com/"
            };
    var httpClient = new HttpClient();
    // 定義每個 url 的使用方法。
    var downloads = urls.Select(url =>
    {
        Console.WriteLine($"{url}:start");
        var res = httpClient.GetStringAsync(url);
        res.ContinueWith(t => Console.WriteLine($"{url}:{sw.ElapsedMilliseconds}ms"));
        return res;
    });
    // 注意,到這裏,序列尚未求值,因此全部任務都還沒真正啓動。

    // 下面,全部的 URL 下載同步開始。
    Task<string>[] downloadTasks = downloads.ToArray();
    // 到這裏,全部的任務已經開始執行了。
    Console.WriteLine($"await Task.WhenAll");
    // 用異步方式等待全部下載完成。
    string[] htmlPages = await Task.WhenAll(downloadTasks);

    Console.WriteLine($"jobs done.");
}

輸出爲:性能

https://www.baidu.com/:start
https://cn.bing.com/:start
await Task.WhenAll
https://www.baidu.com/:270ms
jobs done.
; 因爲返回的是請求的 Task 不是 ContinueWith 的打印 Task
https://cn.bing.com/:1089ms

4. 異常處理

  • 若是有一個任務拋出異常,則 Task.WhenAll 會出錯,並把這個異常放在返回的 Task
  • 若是多個任務拋出異常,則這些異常都會放在返回的 Task
  • 若是這個 Task 在被 await 調用,就只會拋出該異步方法的一個異常
  • 若是要獲得每一個異常,能夠檢查 Task.WhenAll 返回的 TaskException 屬性:

示例:url

static async Task ThrowNotImplementedExceptionAsync()
{
    await Task.Delay(10);
    throw new NotImplementedException();
}

static async Task<int> ThrowInvalidOperationExceptionAsync()
{
    TaskCompletionSource<int> completionSource = new TaskCompletionSource<int>();
    completionSource.TrySetException(new InvalidOperationException());
    return await completionSource.Task;
}
static async Task ObserveOneExceptionAsync()
{
    System.Console.WriteLine("OneException");
    var task1 = ThrowNotImplementedExceptionAsync();
    var task2 = ThrowInvalidOperationExceptionAsync();
    try
    {
        await Task.WhenAll(task1, task2);
    }
    catch (Exception ex)
    {
        // ex 要麼是 NotImplementedException,要麼是 InvalidOperationException
        System.Console.WriteLine(ex.GetType().Name);
    }
}
static async Task ObserveAllExceptionsAsync()
{
    System.Console.WriteLine("AllExceptions");
    var task1 = ThrowNotImplementedExceptionAsync();
    var task2 = ThrowInvalidOperationExceptionAsync();
    Task allTasks = Task.WhenAll(task1, task2);
    try
    {
        await allTasks;
    }
    catch
    {
        AggregateException allExceptions = allTasks.Exception;
        allExceptions.Handle(ex =>
        {
            System.Console.WriteLine(ex.GetType().Name);
            return true;
        });
    }
}

輸出爲:spa

OneException
NotImplementedException
AllExceptions
NotImplementedException
InvalidOperationException

5. 等待任意一個任務完成

// 返回第一個響應的 URL 的數據長度。
private static async Task<int> FirstRespondingUrlAsync()
{
    string urlA = "https://www.baidu.com/";
    string urlB = "https://cn.bing.com/";

    var httpClient = new HttpClient();
    // 併發地開始兩個下載任務。
    Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
    Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
    // 等待任意一個任務完成。 
    Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB);
    // 返回從 URL 獲得的數據的長度。 
    byte[] data = await completedTask;
    Console.WriteLine($"Finish: {(completedTask == downloadTaskA ? nameof(downloadTaskA) : nameof(downloadTaskA))}");
    Console.WriteLine($"downloadTaskA: {downloadTaskA.Status}");
    Console.WriteLine($"downloadTaskB: {downloadTaskB.Status}");
    return data.Length;
}

輸出爲:

Finish: downloadTaskA
downloadTaskA: RanToCompletion
downloadTaskB: WaitingForActivation

若是這個任務完成時有異常,這個異常也不會傳遞給 Task.WhenAny 返回的 Task 對象。所以,一般須要在 Task 對象完成後繼續使用 await

第一個任務完成後,考慮是否要取消剩下的任務。若是其餘任務沒有被取消,也沒有被繼續 await,那它們就處於被遺棄的狀態。被遺棄的任務會繼續運行直到完成,它們的結果會被忽略,拋出的任何異常也會被忽略。

//每一個任務須要等到Trace.WriteLine執行完才能執行下一個
static async Task<int> DelayAndReturnAsync(int val)
{
    await Task.Delay(TimeSpan.FromSeconds(val));
    return val;
}
// 當前,此方法輸出「2」,「3」,「1」。 // 咱們但願它先輸出先完成的,指望 輸出「1」,「2」,「3」。 
static async Task ProcessTasksAsync1()
{
    // 建立任務隊列。
    Task<int> taskA = DelayAndReturnAsync(2);
    Task<int> taskB = DelayAndReturnAsync(3);
    Task<int> taskC = DelayAndReturnAsync(1);
    var tasks = new[] { taskA, taskB, taskC };
    // 按順序 await 每一個任務。 
    foreach (var task in tasks)
    {
        var result = await task;
        Console.WriteLine(result);
    }
}


//不等Trace.WriteLine切任務並行的解決方案
// 如今,這個方法輸出「1」,「2」,「3」。 
static async Task ProcessTasksAsync2()
{
    // 建立任務隊列。
    Task<int> taskA = DelayAndReturnAsync(2);
    Task<int> taskB = DelayAndReturnAsync(3);
    Task<int> taskC = DelayAndReturnAsync(1);
    var tasks = new[] { taskA, taskB, taskC };
    var processingTasks = tasks.Select(async t =>
    {
        var result = await t;
        Console.WriteLine(result);
    }).ToArray();
    // 等待所有處理過程的完成。
    await Task.WhenAll(processingTasks);
}
static async Task ProcessTasksAsyncExe()
{
    Stopwatch sw = Stopwatch.StartNew();
    System.Console.WriteLine("ProcessTasksAsync1");
    await ProcessTasksAsync1();
    System.Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
    sw.Restart();
    System.Console.WriteLine();
    System.Console.WriteLine("ProcessTasksAsync2");
    await ProcessTasksAsync2();
    System.Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
}

輸出爲:

ProcessTasksAsync1
2
3
1
3007ms

ProcessTasksAsync2
1
2
3
3004ms

6. 避免上下文延續

默認狀況下,一個 async 方法在被 await 調用後恢復運行時,會在原來的上下文中運行。 若是是 UI上下文 ,而且有大量的 async 方法在 UI上下文 中恢復,就會引發性能上的問題。

  • ConfigureAwait(true) 延續上下文(執行完異步 await ,回到同步上下文)
  • ConfigureAwait(false) 不延續上下文(執行完異步 await ,因爲沒有記錄以前的同步上下文,後續代碼在 Default上下文 中運行)

7. async void

處理 async void 方法的異常有一個辦法:

  • 一個異常從 async void 方法中傳遞出來時,會在其同步上下文中引起出來
  • async void方法啓動時,同步上下文 就處於激活狀態
    • 若是系統運行環境有特定的 同步上下文(如:UI同步上下文,ASP.Net同步上下文),一般就能夠在全局範圍內處理這些頂層的異常
      • PF 有 Application.DispatcherUnhandledException
      • WinRT 有 Application.UnhandledException
      • ASP.NET 有 Application_Error
相關文章
相關標籤/搜索