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

来源:https://www.cnblogs.com/InCerry/archive/2018/08/09/9450493.html
-Advertisement-
Play Games

[TOC] 本系列首頁鏈接: "[C 多線程編程系列(一) 簡介" ] 1.1 簡介 在之前的幾個章節中,就線程的使用和多線程相關的內容進行了介紹。因為線程涉及到非同步、同步、異常傳遞等問題,所以在項目中使用多線程的代價是比較高昂的,需要編寫大量的代碼來達到正確性和健壯性。 為瞭解決這樣一些的問題,在 ...


目錄


本系列首頁鏈接:[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》這本書,並且分享做的相關筆記;但是由於筆者目前職業規劃和身體原因,可能最近都沒有時間來更新這個系列,沒法做到幾天一更。請大家多多諒解!但是筆者一定會將這個系列全部更新完成的!感謝大家的支持!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.函數的動態參數: 1.*args 位置參數動態傳參 結果:1 2 3 4 5 順序:位置參數=>*args=>預設值參數 *在這裡表示接收位置參數的動態傳參,接收到的是元組 結果為:1 2 3 4 5 將第一個值賦值給a,後面的值都給b 2.**kwargs 關鍵字參數動態傳參 結果為: 1 { ...
  • 不知什麼時候 ,出現了這樣的一個奇怪問題,簡單的httpClient.GetAsync("xxxx")居然報錯了。 一、問題描述 把原來的程式從2.0升級到2.1,突然發現原本正常運行的httpClient.GetAsync("xxxx")居然不工作了。 為了排除項目中其他引用的干擾,新建了一個乾凈 ...
  • 序言 使用.NET Core,團隊可以更容易專註的在.net core上工作。比如核心類庫(如System.Collections)的更改仍然需要與.NET Framework相同的活力,但是ASP.NET Core或Entity Framework Core可以更輕鬆地進行實質性更改,而不受向後兼 ...
  • 一. Log4Net簡介 Log4net是從Java中的Log4j遷移過來的一個.Net版的開源日誌框架,它的功能很強大,可以將日誌分為不同的等級,以不同的格式輸出到不同的存儲介質中,比如:資料庫、txt文件、記憶體緩衝區、郵件、控制台、ANSI終端、遠程接收端等等,我們這裡主要介紹最常用的兩種:tx ...
  • AspNetCore中使用Ocelot之 InentityServer4(1) 前言: OceLot網關是基於AspNetCore 產生的可擴展的高性能的企業級Api網關,目前已經基於2.0 升級版本升級,在使用AspNetCore 開發的時候可以使用2.0版本了, 開源項目Ocelot 張大隊長是 ...
  •  Net Core平臺靈活簡單的日誌記錄框架NLog+SqlServer初體驗 前幾天分享的"[Net Core平臺靈活簡單的日誌記錄框架NLog+Mysql組合初體驗][http://www.cnblogs.com/yilezhu/p/9416439.html]" 反響還行。有網友就說有了NLo ...
  • 學到新東西就記錄一下。也許正好有人需要~~~~~~ 由於需要記錄當前線上用戶,emmmm又是沒做過的。。。 本來想用資料庫的形式,但是想想這麼簡單的功能百度肯定有。遨游一波百度,有所收穫。。。。 雖然老是那麼幾篇文章重覆。。。。 大概就是在用戶登錄時Session記錄下數據,前臺獲取展示。下麵這個文 ...
  • 原文:https://www.cnblogs.com/gguozhenqian/p/4288451.html 需要添加引用System.Windows.Forms 1 public class AutoSizeFormClass 2 { 3 //(1).聲明結構,只記錄窗體和其控制項的初始位置和大小。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...