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


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


1.1 簡介

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

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

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

任務是對象,其中封裝了以異步方式執行的工做,可是委託也是封裝了代碼的對象。任務和委託的區別在於,委託是同步的,而任務是異步的。多線程

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

1.2 建立任務

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

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

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》這本書,而且分享作的相關筆記;可是因爲筆者目前職業規劃和身體緣由,可能最近都沒有時間來更新這個系列,無法作到幾天一更。請你們多多諒解!可是筆者必定會將這個系列所有更新完成的!感謝你們的支持!

相關文章
相關標籤/搜索