C#多線程編程系列(五)- 使用任務並行庫



本系列首頁鏈接:[C#多線程編程系列(一)- 簡介 ]


1.1 簡介

在之前的幾個章節中,就線程的使用和多線程相關的內容進行了介紹。因為線程涉及到異步、同步、異常傳遞等問題,所以在項目中使用多線程的代價是比較高昂的,需要編寫大量的代碼來達到正確性和健壯性。

為了解決這樣一些的問題,在.Net Framework 4.0中引入了一個關於一步操作的API。它叫做任務並行庫(Task Parallel Library)。然后在.Net Framwork 4.5中對它進行了輕微的改進,本文的案例都是用最新版本的TPL庫,而且我們還可以使用C# 5.0的新特性await/async來簡化TAP編程,當然這是之后才介紹的。

TPL內部使用了線程池,但是效率更高。在把線程歸還回線程池之前,它會在同一線程中順序執行多少Task,這樣避免了一些小任務上下文切換浪費時間片的問題。

任務是對象,其中封裝了以異步方式執行的工作,但是委托也是封裝了代碼的對象。任務和委托的區別在於,委托是同步的,而任務是異步的。

在本章中,我們將會討論如何使用TPL庫來進行任務之間的組合同步,如何將遺留的APM和EAP模式轉換為TPL模式等等。

1.2 創建任務

在本節中,主要是演示了如何創建一個任務。其主要用到了System.Threading.Tasks命名空間下的Task類。該類可以被實例化並且提供了一組靜態方法,可以方便快捷的創建任務。

在下面實例代碼中,分別延時了三種常見的任務創建方式,並且創建任務是可以指定任務創建的選項,從而達到最優的創建方式。

TaskCreationOptions中一共有7個枚舉,枚舉是可以使用|運算符組合定義的。其枚舉如下表所示。

成員名稱 說明
AttachedToParent 指定將任務附加到任務層次結構中的某個父級。 默認情況下,子任務(即由外部任務創建的內部任務)將獨立於其父任務執行。 可以使用 TaskContinuationOptions.AttachedToParent 選項以便將父任務和子任務同步。請注意,如果使用 DenyChildAttach 選項配置父任務,則子任務中的 AttachedToParent 選項不起作用,並且子任務將作為分離的子任務執行。有關詳細信息,請參閱附加和分離的子任務
DenyChildAttach 指定任何嘗試作為附加的子任務執行(即,使用 AttachedToParent 選項創建)的子任務都無法附加到父任務,會改成作為分離的子任務執行。 有關詳細信息,請參閱附加和分離的子任務
HideScheduler 防止環境計划程序被視為已創建任務的當前計划程序。 這意味着像 StartNew 或 ContinueWith 創建任務的執行操作將被視為 Default 當前計划程序。
LongRunning 指定任務將是長時間運行的、粗粒度的操作,涉及比細化的系統更少、更大的組件。 它會向 TaskScheduler 提示,過度訂閱可能是合理的。 可以通過過度訂閱創建比可用硬件線程數更多的線程。 它還將提示任務計划程序:該任務需要附加線程,以使任務不阻塞本地線程池隊列中其他線程或工作項的向前推動。
None 指定應使用默認行為。
PreferFairness 提示 TaskScheduler 以一種盡可能公平的方式安排任務,這意味着較早安排的任務將更可能較早運行,而較晚安排運行的任務將更可能較晚運行。
RunContinuationsAsynchronously 強制異步執行添加到當前任務的延續任務。請注意,RunContinuationsAsynchronously 成員在以 .NET Framework 4.6 開頭的 TaskCreationOptions 枚舉中可用。
static void Main(string[] args)
{
    // 使用構造方法創建任務
    var t1 = new Task(() => TaskMethod("Task 1"));
    var t2 = new Task(() => TaskMethod("Task 2"));

    // 需要手動啟動
    t2.Start();
    t1.Start();

    // 使用Task.Run 方法啟動任務  不需要手動啟動
    Task.Run(() => TaskMethod("Task 3"));

    // 使用 Task.Factory.StartNew方法 啟動任務 實際上就是Task.Run
    Task.Factory.StartNew(() => TaskMethod("Task 4"));

    // 在StartNew的基礎上 添加 TaskCreationOptions.LongRunning 告訴 Factory該任務需要長時間運行
    // 那么它就會可能會創建一個 非線程池線程來執行任務  
    Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);

    ReadLine();
}

static void TaskMethod(string name)
{
    WriteLine($"任務 {name} 運行,線程 id {CurrentThread.ManagedThreadId}. 是否為線程池線程: {CurrentThread.IsThreadPoolThread}.");
}

運行結果如下圖所示。

1533608520548

1.3 使用任務執行基本的操作

在本節中,使用任務執行基本的操作,並且獲取任務執行完成后的結果值。本節內容比較簡單,在此不做過多介紹。

演示代碼如下,在主線程中要獲取結果值,常用的方式就是訪問task.Result屬性,如果任務線程還沒執行完畢,那么會阻塞主線程,直到線程執行完。如果任務線程執行完畢,那么將直接拿到運算的結果值。

Task 3中,使用了task.Status來打印線程的狀態,線程每個狀態的具體含義,將在下一節中介紹。

static void Main(string[] args)
{
    // 直接執行方法 作為參照
    TaskMethod("主線程任務");

    // 訪問 Result屬性 達到運行結果
    Task<int> task = CreateTask("Task 1");
    task.Start();
    int result = task.Result;
    WriteLine($"運算結果: {result}");

    // 使用當前線程,同步執行任務
    task = CreateTask("Task 2");
    task.RunSynchronously();
    result = task.Result;
    WriteLine($"運算結果:{result}");

    // 通過循環等待 獲取運行結果
    task = CreateTask("Task 3");
    WriteLine(task.Status);
    task.Start();

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }

    WriteLine(task.Status);
    result = task.Result;
    WriteLine($"運算結果:{result}");

    Console.ReadLine();
}

static Task<int> CreateTask(string name)
{
    return new Task<int>(() => TaskMethod(name));
}

static int TaskMethod(string name)
{
    WriteLine($"{name} 運行在線程 {CurrentThread.ManagedThreadId}上. 是否為線程池線程 {CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(2));

    return 42;
}

運行結果如下,可見Task 1Task 2均是運行在主線程上,並非線程池線程。

1533798340309

1.4 組合任務

在本節中,體現了任務其中一個強大的功能,那就是組合任務。通過組合任務可很好的描述任務與任務之間的異步、同步關系,大大降低了編程的難度。

組合任務主要是通過task.ContinueWith()task.WhenAny()task.WhenAll()等和task.GetAwaiter().OnCompleted()方法來實現。

在使用task.ContinueWith()方法時,需要注意它也可傳遞一系列的枚舉選項TaskContinuationOptions,該枚舉選項和TaskCreationOptions類似,其具體定義如下表所示。

成員名稱 說明
AttachedToParent 如果延續為子任務,則指定將延續附加到任務層次結構中的父級。 只有當延續前面的任務也是子任務時,延續才可以是子任務。 默認情況下,子任務(即由外部任務創建的內部任務)將獨立於其父任務執行。 可以使用 TaskContinuationOptions.AttachedToParent 選項以便將父任務和子任務同步。請注意,如果使用 DenyChildAttach 選項配置父任務,則子任務中的 AttachedToParent 選項不起作用,並且子任務將作為分離的子任務執行。有關更多信息,請參見Attached and Detached Child Tasks
DenyChildAttach 指定任何使用 TaskCreationOptions.AttachedToParent 選項創建,並嘗試作為附加的子任務執行的子任務(即,由此延續創建的任何嵌套內部任務)都無法附加到父任務,會改成作為分離的子任務執行。 有關詳細信息,請參閱附加和分離的子任務
ExecuteSynchronously 指定應同步執行延續任務。 指定此選項后,延續任務在導致前面的任務轉換為其最終狀態的相同線程上運行。如果在創建延續任務時已經完成前面的任務,則延續任務將在創建此延續任務的線程上運行。 如果前面任務的 CancellationTokenSource 已在一個 finally(在 Visual Basic 中為 Finally)塊中釋放,則使用此選項的延續任務將在該 finally 塊中運行。 只應同步執行運行時間非常短的延續任務。由於任務以同步方式執行,因此無需調用諸如 Task.Wait 的方法來確保調用線程等待任務完成。
HideScheduler 指定由延續通過調用方法(如 Task.RunTask.ContinueWith)創建的任務將默認計划程序 (TaskScheduler.Default) 視為當前的計划程序,而不是正在運行該延續的計划程序。
LazyCancellation 在延續取消的情況下,防止延續的完成直到完成先前的任務。
LongRunning 指定延續將是長期運行的、粗粒度的操作。 它會向 TaskScheduler 提示,過度訂閱可能是合理的。
None 如果未指定延續選項,應在執行延續任務時使用指定的默認行為。 延續任務在前面的任務完成后以異步方式運行,與前面任務最終的 Task.Status 屬性值無關。 如果延續為子任務,則會將其創建為分離的嵌套任務。
NotOnCanceled 指定不應在延續任務前面的任務已取消的情況下安排延續任務。 如果前面任務完成的 Task.Status 屬性是 TaskStatus.Canceled,則前面的任務會取消。 此選項對多任務延續無效。
NotOnFaulted 指定不應在延續任務前面的任務引發了未處理異常的情況下安排延續任務。 如果前面任務完成的 Task.Status 屬性是 TaskStatus.Faulted,則前面的任務會引發未處理的異常。 此選項對多任務延續無效。
NotOnRanToCompletion 指定不應在延續任務前面的任務已完成運行的情況下安排延續任務。 如果前面任務完成的 Task.Status 屬性是 TaskStatus.RanToCompletion,則前面的任務會運行直至完成。 此選項對多任務延續無效。
OnlyOnCanceled 指定只應在延續前面的任務已取消的情況下安排延續任務。 如果前面任務完成的 Task.Status 屬性是 TaskStatus.Canceled,則前面的任務會取消。 此選項對多任務延續無效。
OnlyOnFaulted 指定只有在延續任務前面的任務引發了未處理異常的情況下才應安排延續任務。 如果前面任務完成的 Task.Status 屬性是 TaskStatus.Faulted,則前面的任務會引發未處理的異常。OnlyOnFaulted 選項可保證前面任務中的 Task.Exception 屬性不是 null。 你可以使用該屬性來捕獲異常,並確定導致任務出錯的異常。 如果你不訪問 Exception 屬性,則不會處理異常。 此外,如果嘗試訪問已取消或出錯的任務的 Result 屬性,則會引發一個新異常。此選項對多任務延續無效。
OnlyOnRanToCompletion 指定只應在延續任務前面的任務已完成運行的情況下才安排延續任務。 如果前面任務完成的 Task.Status 屬性是 TaskStatus.RanToCompletion,則前面的任務會運行直至完成。 此選項對多任務延續無效。
PreferFairness 提示 TaskScheduler 按任務計划的順序安排任務,因此較早安排的任務將更可能較早運行,而較晚安排運行的任務將更可能較晚運行。
RunContinuationsAsynchronously 指定應異步運行延續任務。 此選項優先於 TaskContinuationOptions.ExecuteSynchronously。

演示代碼如下所示,使用ContinueWith()OnCompleted()方法組合了任務來運行,搭配不同的TaskCreationOptionsTaskContinuationOptions來實現不同的效果。

static void Main(string[] args)
{
    WriteLine($"主線程 線程 Id {CurrentThread.ManagedThreadId}");

    // 創建兩個任務
    var firstTask = new Task<int>(() => TaskMethod("Frist Task",3));
    var secondTask = new Task<int>(()=> TaskMethod("Second Task",2));

    // 在默認的情況下 ContiueWith會在前面任務運行后再運行
    firstTask.ContinueWith(t => WriteLine($"第一次運行答案是 {t.Result}. 線程Id {CurrentThread.ManagedThreadId}. 是否為線程池線程: {CurrentThread.IsThreadPoolThread}"));

    // 啟動任務
    firstTask.Start();
    secondTask.Start();

    Sleep(TimeSpan.FromSeconds(4));

    // 這里會緊接着 Second Task運行后運行, 但是由於添加了 OnlyOnRanToCompletion 和 ExecuteSynchronously 所以會由運行SecondTask的線程來 運行這個任務
    Task continuation = secondTask.ContinueWith(t => WriteLine($"第二次運行的答案是 {t.Result}. 線程Id {CurrentThread.ManagedThreadId}. 是否為線程池線程:{CurrentThread.IsThreadPoolThread}"),TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously);

    // OnCompleted 是一個事件  當contiuation運行完成后 執行OnCompleted Action事件
    continuation.GetAwaiter().OnCompleted(() => WriteLine($"后繼任務完成. 線程Id {CurrentThread.ManagedThreadId}. 是否為線程池線程 {CurrentThread.IsThreadPoolThread}"));

    Sleep(TimeSpan.FromSeconds(2));
    WriteLine();

    firstTask = new Task<int>(() => 
    {
        // 使用了TaskCreationOptions.AttachedToParent 將這個Task和父Task關聯, 當這個Task沒有結束時  父Task 狀態為 WaitingForChildrenToComplete
        var innerTask = Task.Factory.StartNew(() => TaskMethod("Second Task",5), TaskCreationOptions.AttachedToParent);

        innerTask.ContinueWith(t => TaskMethod("Thrid Task", 2), TaskContinuationOptions.AttachedToParent);

        return TaskMethod("First Task",2);
    });

    firstTask.Start();

    // 檢查firstTask線程狀態  根據上面的分析 首先是  Running -> WatingForChildrenToComplete -> RanToCompletion
    while (! firstTask.IsCompleted)
    {
        WriteLine(firstTask.Status);

        Sleep(TimeSpan.FromSeconds(0.5));
    }

    WriteLine(firstTask.Status);

    Console.ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任務 {name} 正在運行,線程池線程 Id {CurrentThread.ManagedThreadId},是否為線程池線程: {CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));

    return 42 * seconds;
}

運行結果如下圖所示,與預期結果一致。其中使用了task.Status來打印任務運行的狀態,對於task.Status的狀態具體含義如下表所示。

成員名稱 說明
Canceled 該任務已通過對其自身的 CancellationToken 引發 OperationCanceledException 對取消進行了確認,此時該標記處於已發送信號狀態;或者在該任務開始執行之前,已向該任務的 CancellationToken 發出了信號。 有關詳細信息,請參閱任務取消
Created 該任務已初始化,但尚未被計划。
Faulted 由於未處理異常的原因而完成的任務。
RanToCompletion 已成功完成執行的任務。
Running 該任務正在運行,但尚未完成。
WaitingForActivation 該任務正在等待 .NET Framework 基礎結構在內部將其激活並進行計划。
WaitingForChildrenToComplete 該任務已完成執行,正在隱式等待附加的子任務完成。
WaitingToRun 該任務已被計划執行,但尚未開始執行。

1533798776604

1.5 將APM模式轉換為任務

在前面的章節中,介紹了基於IAsyncResult接口實現了BeginXXXX/EndXXXX方法的就叫APM模式。APM模式非常古老,那么如何將它轉換為TAP模式呢?對於常見的幾種APM模式異步任務,我們一般選擇使用Task.Factory.FromAsync()方法來實現將APM模式轉換為TAP模式

演示代碼如下所示,比較簡單不作過多介紹。

static void Main(string[] args)
{
    int threadId;
    AsynchronousTask d = Test;
    IncompatibleAsychronousTask e = Test;

    // 使用 Task.Factory.FromAsync方法 轉換為Task
    WriteLine("Option 1");
    Task<string> task = Task<string>.Factory.FromAsync(d.BeginInvoke("異步任務線程", CallBack, "委托異步調用"), d.EndInvoke);

    task.ContinueWith(t => WriteLine($"回調函數執行完畢,現在運行續接函數!結果:{t.Result}"));

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }
    WriteLine(task.Status);
    Sleep(TimeSpan.FromSeconds(1));

    WriteLine("----------------------------------------------");
    WriteLine();

    // 使用 Task.Factory.FromAsync重載方法 轉換為Task
    WriteLine("Option 2");

    task = Task<string>.Factory.FromAsync(d.BeginInvoke,d.EndInvoke,"異步任務線程","委托異步調用");

    task.ContinueWith(t => WriteLine($"任務完成,現在運行續接函數!結果:{t.Result}"));

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }
    WriteLine(task.Status);
    Sleep(TimeSpan.FromSeconds(1));

    WriteLine("----------------------------------------------");
    WriteLine();

    // 同樣可以使用 FromAsync方法 將 BeginInvoke 轉換為 IAsyncResult 最后轉換為 Task
    WriteLine("Option 3");

    IAsyncResult ar = e.BeginInvoke(out threadId, CallBack, "委托異步調用");
    task = Task<string>.Factory.FromAsync(ar, _ => e.EndInvoke(out threadId, ar));

    task.ContinueWith(t => WriteLine($"任務完成,現在運行續接函數!結果:{t.Result},線程Id {threadId}"));

    while (!task.IsCompleted)
    {
        WriteLine(task.Status);
        Sleep(TimeSpan.FromSeconds(0.5));
    }
    WriteLine(task.Status);

    ReadLine();
}

delegate string AsynchronousTask(string threadName);
delegate string IncompatibleAsychronousTask(out int threadId);

static void CallBack(IAsyncResult ar)
{
    WriteLine("開始運行回調函數...");
    WriteLine($"傳遞給回調函數的狀態{ar.AsyncState}");
    WriteLine($"是否為線程池線程:{CurrentThread.IsThreadPoolThread}");
    WriteLine($"線程池工作線程Id:{CurrentThread.ManagedThreadId}");
}

static string Test(string threadName)
{
    WriteLine("開始運行...");
    WriteLine($"是否為線程池線程:{CurrentThread.IsThreadPoolThread}");
    Sleep(TimeSpan.FromSeconds(2));

    CurrentThread.Name = threadName;
    return $"線程名:{CurrentThread.Name}";
}

static string Test(out int threadId)
{
    WriteLine("開始運行...");
    WriteLine($"是否為線程池線程:{CurrentThread.IsThreadPoolThread}");
    Sleep(TimeSpan.FromSeconds(2));

    threadId = CurrentThread.ManagedThreadId;
    return $"線程池線程工作Id是:{threadId}";
}

運行結果如下圖所示。

1533778462479

1.6 將EAP模式轉換為任務

在上幾章中有提到,通過BackgroundWorker類通過事件的方式實現的異步,我們叫它EAP模式。那么如何將EAP模式轉換為任務呢?很簡單,我們只需要通過TaskCompletionSource類,即可將EAP模式轉換為任務。

演示代碼如下所示。

static void Main(string[] args)
{
    var tcs = new TaskCompletionSource<int>();

    var worker = new BackgroundWorker();
    worker.DoWork += (sender, eventArgs) =>
    {
        eventArgs.Result = TaskMethod("后台工作", 5);
    };

    // 通過此方法 將EAP模式轉換為 任務
    worker.RunWorkerCompleted += (sender, eventArgs) =>
    {
        if (eventArgs.Error != null)
        {
            tcs.SetException(eventArgs.Error);
        }
        else if (eventArgs.Cancelled)
        {
            tcs.SetCanceled();
        }
        else
        {
            tcs.SetResult((int)eventArgs.Result);
        }
    };

    worker.RunWorkerAsync();

    // 調用結果
    int result = tcs.Task.Result;

    WriteLine($"結果是:{result}");

    ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任務{name}運行在線程{CurrentThread.ManagedThreadId}上. 是否為線程池線程{CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));

    return 42 * seconds;
}

運行結果如下圖所示。

1533785637929

1.7 實現取消選項

在TAP模式中,實現取消選項和之前的異步模式一樣,都是使用CancellationToken來實現,但是不同的是Task構造函數允許傳入一個CancellationToken,從而在任務實際啟動之前取消它。

演示代碼如下所示。

static void Main(string[] args)
{
    var cts = new CancellationTokenSource();
    // new Task時  可以傳入一個 CancellationToken對象  可以在線程創建時  變取消任務
    var longTask = new Task<int>(() => TaskMethod("Task 1", 10, cts.Token), cts.Token);
    WriteLine(longTask.Status);
    cts.Cancel();
    WriteLine(longTask.Status);
    WriteLine("第一個任務在運行前被取消.");

    // 同樣的 可以通過CancellationToken對象 取消正在運行的任務
    cts = new CancellationTokenSource();
    longTask = new Task<int>(() => TaskMethod("Task 2", 10, cts.Token), cts.Token);
    longTask.Start();

    for (int i = 0; i < 5; i++)
    {
        Sleep(TimeSpan.FromSeconds(0.5));
        WriteLine(longTask.Status);
    }
    cts.Cancel();
    for (int i = 0; i < 5; i++)
    {
        Sleep(TimeSpan.FromSeconds(0.5));
        WriteLine(longTask.Status);
    }

    WriteLine($"這個任務已完成,結果為{longTask.Result}");

    ReadLine();
}

static int TaskMethod(string name, int seconds, CancellationToken token)
{
    WriteLine($"任務運行在{CurrentThread.ManagedThreadId}上. 是否為線程池線程:{CurrentThread.IsThreadPoolThread}");

    for (int i = 0; i < seconds; i++)
    {
        Sleep(TimeSpan.FromSeconds(1));
        if (token.IsCancellationRequested)
        {
            return -1;
        }
    }

    return 42 * seconds;
}

運行結果如下圖所示,這里需要注意的是,如果是在任務執行之前取消了任務,那么它的最終狀態是Canceled。如果是在執行過程中取消任務,那么它的狀態是RanCompletion

1533783996906

1.8 處理任務中的異常

在任務中,處理異常和其它異步方式處理異常類似,如果能在所發生異常的線程中處理,那么不要在其它地方處理。但是對於一些不可預料的異常,那么可以通過幾種方式來處理。

可以通過訪問task.Result屬性來處理異常,因為訪問這個屬性的Get方法會使當前線程等待直到該任務完成,並將異常傳播給當前線程,這樣就可以通過try catch語句塊來捕獲異常。另外使用task.GetAwaiter().GetResult()方法和第使用task.Result類似,同樣可以捕獲異常。如果是要捕獲多個任務中的異常錯誤,那么可以通過ContinueWith()方法來處理。

具體如何實現,演示代碼如下所示。

static void Main(string[] args)
{
    Task<int> task;
    // 在主線程中調用 task.Result task中的異常信息會直接拋出到 主線程中
    try
    {
        task = Task.Run(() => TaskMethod("Task 1", 2));
        int result = task.Result;
        WriteLine($"結果為: {result}");
    }
    catch (Exception ex)
    {
        WriteLine($"異常被捕捉:{ex.Message}");
    }
    WriteLine("------------------------------------------------");
    WriteLine();

    // 同上 只是訪問Result的方式不同
    try
    {
        task = Task.Run(() => TaskMethod("Task 2", 2));
        int result = task.GetAwaiter().GetResult();
        WriteLine($"結果為:{result}");
    }
    catch (Exception ex)
    {
        WriteLine($"異常被捕捉: {ex.Message}");
    }
    WriteLine("----------------------------------------------");
    WriteLine();

    var t1 = new Task<int>(() => TaskMethod("Task 3", 3));
    var t2 = new Task<int>(() => TaskMethod("Task 4", 4));

    var complexTask = Task.WhenAll(t1, t2);
    // 通過ContinueWith TaskContinuationOptions.OnlyOnFaulted的方式 如果task出現異常 那么才會執行該方法
    var exceptionHandler = complexTask.ContinueWith(t => {
        WriteLine($"異常被捕捉:{t.Exception.Message}");
        foreach (var ex in t.Exception.InnerExceptions)
        {
            WriteLine($"-------------------------- {ex.Message}");
        }
    },TaskContinuationOptions.OnlyOnFaulted);

    t1.Start();
    t2.Start();

    ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任務運行在{CurrentThread.ManagedThreadId}上. 是否為線程池線程:{CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));
    // 人為拋出一個異常
    throw new Exception("Boom!");
    return 42 * seconds;
}

運行結果如下所示,需要注意的是,如果在ContinueWith()方法中捕獲多個任務產生的異常,那么它的異常類型是AggregateException,具體的異常信息包含在InnerExceptions里面,要注意和InnerException區分。

1533785572866

1.9 並行運行任務

本節中主要介紹了兩個方法的使用,一個是等待組中全部任務都執行結束的Task.WhenAll()方法,另一個是只要組中一個方法執行結束都執行的Task.WhenAny()方法。

具體使用,如下演示代碼所示。

static void Main(string[] args)
{
    // 第一種方式 通過Task.WhenAll 等待所有任務運行完成
    var firstTask = new Task<int>(() => TaskMethod("First Task", 3));
    var secondTask = new Task<int>(() => TaskMethod("Second Task", 2));

    // 當firstTask 和 secondTask 運行完成后 才執行 whenAllTask的ContinueWith
    var whenAllTask = Task.WhenAll(firstTask, secondTask);
    whenAllTask.ContinueWith(t => WriteLine($"第一個任務答案為{t.Result[0]},第二個任務答案為{t.Result[1]}"), TaskContinuationOptions.OnlyOnRanToCompletion);

    firstTask.Start();
    secondTask.Start();

    Sleep(TimeSpan.FromSeconds(4));

    // 使用WhenAny方法  只要列表中有一個任務完成 那么該方法就會取出那個完成的任務
    var tasks = new List<Task<int>>();
    for (int i = 0; i < 4; i++)
    {
        int counter = 1;
        var task = new Task<int>(() => TaskMethod($"Task {counter}",counter));
        tasks.Add(task);
        task.Start();
    }

    while (tasks.Count > 0)
    {
        var completedTask = Task.WhenAny(tasks).Result;
        tasks.Remove(completedTask);
        WriteLine($"一個任務已經完成,結果為 {completedTask.Result}");
    }

    ReadLine();
}

static int TaskMethod(string name, int seconds)
{
    WriteLine($"任務運行在{CurrentThread.ManagedThreadId}上. 是否為線程池線程:{CurrentThread.IsThreadPoolThread}");

    Sleep(TimeSpan.FromSeconds(seconds));
    return 42 * seconds;
}

運行結果如下圖所示。

1533793481274

1.10 使用TaskScheduler配置任務執行

Task中,負責任務調度是TaskScheduler對象,FCL提供了兩個派生自TaskScheduler的類型:線程池任務調度器(Thread Pool Task Scheduler)同步上下文任務調度器(Synchronization Scheduler)。默認情況下所有應用程序都使用線程池任務調度器,但是在UI組件中,不使用線程池中的線程,避免跨線程更新UI,需要使用同步上下文任務調度器。可以通過執行TaskSchedulerFromCurrentSynchronizationContext()靜態方法來獲得對同步上下文任務調度器的引用。

演示程序如下所示,為了延時同步上下文任務調度器,我們此次使用WPF來創建項目。

MainWindow.xaml 代碼如下所示。

<Window x:Class="Recipe9.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Recipe9"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBlock Name="ContentTextBlock" HorizontalAlignment="Left" Margin="44,134,0,0" VerticalAlignment="Top" Width="425" Height="40"/>
        <Button Content="Sync" HorizontalAlignment="Left" Margin="45,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonSync_Click"/>
        <Button Content="Async" HorizontalAlignment="Left" Margin="165,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsync_Click"/>
        <Button Content="Async OK" HorizontalAlignment="Left" Margin="285,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsyncOK_Click"/>
    </Grid>
</Window>

MainWindow.xaml.cs 代碼如下所示。

/// <summary>
/// MainWindow.xaml 的交互邏輯
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    // 同步執行 計算密集任務 導致UI線程阻塞
    private void ButtonSync_Click(object sender, RoutedEventArgs e)
    {
        ContentTextBlock.Text = string.Empty;

        try
        {
            string result = TaskMethod().Result;
            ContentTextBlock.Text = result;
        }
        catch (Exception ex)
        {
            ContentTextBlock.Text = ex.InnerException.Message;
        }
    }

    // 異步的方式來執行 計算密集任務 UI線程不會阻塞 但是 不能跨線程更新UI 所以會有異常
    private void ButtonAsync_Click(object sender, RoutedEventArgs e)
    {
        ContentTextBlock.Text = string.Empty;
        Mouse.OverrideCursor = Cursors.Wait;

        Task<string> task = TaskMethod();
        task.ContinueWith(t => {
            ContentTextBlock.Text = t.Exception.InnerException.Message;
            Mouse.OverrideCursor = null;
        }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext());
    }

    // 通過 異步 和 FromCurrentSynchronizationContext方法 創建了線程同步的上下文  沒有跨線程更新UI 
    private void ButtonAsyncOK_Click(object sender, RoutedEventArgs e)
    {
        ContentTextBlock.Text = string.Empty;
        Mouse.OverrideCursor = Cursors.Wait;
        Task<string> task = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext());

        task.ContinueWith(t => Mouse.OverrideCursor = null,
            CancellationToken.None,
            TaskContinuationOptions.None,
            TaskScheduler.FromCurrentSynchronizationContext());
    }

    Task<string> TaskMethod()
    {
        return TaskMethod(TaskScheduler.Default);
    }

    Task<string> TaskMethod(TaskScheduler scheduler)
    {
        Task delay = Task.Delay(TimeSpan.FromSeconds(5));

        return delay.ContinueWith(t =>
        {
            string str = $"任務運行在{CurrentThread.ManagedThreadId}上. 是否為線程池線程:{CurrentThread.IsThreadPoolThread}";

            Console.WriteLine(str);

            ContentTextBlock.Text = str;
            return str;
        }, scheduler);
    }
}

運行結果如下所示,從左至右依次單擊按鈕,前兩個按鈕將會引發異常。
1533806840998

具體信息如下所示。

1533794812153

參考書籍

本文主要參考了以下幾本書,在此對這些作者表示由衷的感謝,感謝你們為.Net的發揚光大所做的貢獻!

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》
  5. 《C#多線程編程實戰》

源碼下載點擊鏈接 示例源碼下載

筆者水平有限,如果錯誤歡迎各位批評指正!

本來想趁待業期間的時間讀完《Multithreading with C# Cookbook Second Edition》這本書,並且分享做的相關筆記;但是由於筆者目前職業規划和身體原因,可能最近都沒有時間來更新這個系列,沒法做到幾天一更。請大家多多諒解!但是筆者一定會將這個系列全部更新完成的!感謝大家的支持!


免責聲明!

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



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