C#線程——Task(任務)


關於C#多線程這一塊,怎麼說呢,雖然經常用到,但是一直沒有好好整理,趁着年前沒什麼事,就拿出來梳理了一下。雖說整理但覺得還是寫的很亂,都是基礎用法,方便自己以後使用吧。提前祝大家新年快樂

一、認識Task

  任務,基於線程池,並在線程池的基礎上進行了優化,提供了更多的API,這些 API 支持等待、取消、繼續、可靠的異常處理、詳細狀態、自定義計劃等功能。其使我們對並行編程變得更簡單,且不用關心底層是怎麼實現的。並行編程就像現實中,我們開發項目,就是一個並行的例子,把不同的模塊分給不同的人,同時進行,才能在短的時間內做出大的項目。

二、創建Task

  創建Task的方法有多種,下面來看一下官網例子:三個任務執行一個名爲 action的 Action<T> 委託,該委託接受 Object類型的參數。 第四個任務執行在對任務創建方法的調用中以內聯方式定義的 lambda 表達式(Action 委託)。 每個任務實例化並以不同的方式運行:
1、任務 t1 通過調用任務類構造函數進行實例化,但只有在啓動任務 t2 之後,才通過調用其 Start() 方法來啓動。
2、通過調用TaskFactory.StartNew(Action<Object>, Object) 方法在單個方法調用中實例化和啓動任務 t2。
3、通過調用Run(Action) 方法在單個方法調用中實例化和啓動任務 t3。Task.Run跟Task.Factory.StarNew和new Task相差不多,不同的是前兩種是放進線程池立即執行,而Task.Run則是等線程池空閒後在執行。
4、通過調用RunSynchronously() 方法,在主線程上同步執行任務 t4。
由於 task t4 同步執行,因此它在主應用程序線程上執行。 其餘任務通常在一個或多個線程池線程上異步執行。

Action<object> action = (object obj) =>
{
    Console.WriteLine("Task={0}, obj={1}, Thread={2}",Task.CurrentId, obj,Thread.CurrentThread.ManagedThreadId);
};

// Create a task but do not start it.
Task t1 = new Task(action, "task");

// Construct a started task
Task t2 = Task.Factory.StartNew(action, "Factory");
// Block the main thread to demonstrate that t2 is executing
t2.Wait();

// Launch t1 
t1.Start();
Console.WriteLine("t1 has been launched. (Main Thread={0})",Thread.CurrentThread.ManagedThreadId);
// Wait for the task to finish.
t1.Wait();

// Construct a started task using Task.Run.
string taskData = "run";
Task t3 = Task.Run(() => {Console.WriteLine("Task={0}, obj={1}, Thread={2}",Task.CurrentId, taskData,Thread.CurrentThread.ManagedThreadId);});
// Wait for the task to finish.
t3.Wait();

// Construct an unstarted task
Task t4 = new Task(action, "RunSynchronously");
// Run it synchronously
t4.RunSynchronously();
// Although the task was run synchronously, it is a good practice to wait for it in the event exceptions were thrown by the task.
t4.Wait();

輸出結果如下:在這裏插入圖片描述
構造函數:比較常用的是前兩種
在這裏插入圖片描述
下面通過一個簡單例子來看一下Task的聲明週期,編寫如下代碼:

var task1 = new Task(() =>
{
    Console.WriteLine("Begin");
    System.Threading.Thread.Sleep(2000);
    Console.WriteLine("Finish");
});
Console.WriteLine("Before start:" + task1.Status+ "\tIsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task1.IsCanceled, task1.IsCompleted, task1.IsFaulted);
task1.Start();
Console.WriteLine("After start:" + task1.Status + "\tIsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task1.IsCanceled, task1.IsCompleted, task1.IsFaulted);
task1.Wait();
Console.WriteLine("After Finish:" + task1.Status + "\tIsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task1.IsCanceled, task1.IsCompleted, task1.IsFaulted);

Console.Read();

其輸出結果如下:
在這裏插入圖片描述
可以看到調用Start前的狀態是Created,然後等待分配線程去執行,到最後執行完成。
從我們可以得出Task的簡略生命週期:
Created:表示默認初始化任務,但是“工廠創建的”實例直接跳過。
WaitingToRun: 這種狀態表示等待任務調度器分配線程給任務執行。
RanToCompletion:任務執行完畢。

  由於 Task 對象執行的工作通常在線程池線程上異步執行,而不是在主應用程序線程上同步執行,因此您可以使用 Status 屬性,還可以使用 IsCanceled、IsCompleted和 IsFaulted 屬性,用於確定任務的狀態。

三、任務控制

  Task最吸引人的地方就是他的任務控制了,你可以很好的控制task的執行順序,讓多個task有序的工作。下面來詳細說一下:

1、Task.Wait

若要等待單個任務完成,可以調用其 Task.Wait 方法。 調用 Wait 方法會阻止調用線程,直到單類實例執行完畢。無參數Wait() 方法無條件等待,直到任務完成。 Wait(Int32) Wait(TimeSpan) 方法會阻止調用線程,直到任務完成或超時間隔結束(以先達到者爲準)。通過調用 Wait(CancellationToken) Wait(Int32, CancellationToken) 方法來提供取消標記。 如果在執行 Wait 方法時令牌的 IsCancellationRequested 屬性 true 或變爲 true,則該方法將引發 OperationCanceledException。

// Wait on a single task with a timeout specified.
Task taskA = Task.Run(() => Thread.Sleep(2000));
try
{
    taskA.Wait(1000);       // Wait for 1 second.
    bool completed = taskA.IsCompleted;
    Console.WriteLine("Task A completed: {0}, Status: {1}",completed, taskA.Status);
    if (!completed)
        Console.WriteLine("Timed out before task A completed.");
}
catch (AggregateException)
{
    Console.WriteLine("Exception in taskA.");
}

運行結果如下:
在這裏插入圖片描述

2、Task.WaitAll

看字面意思就知道,就是等待所有的任務都執行完成,下面我們來寫一段代碼演示一下:

var tasks = new Task[3];
var rnd = new Random();
for (int ctr = 0; ctr <= 2; ctr++)
    tasks[ctr] = Task.Run(() => Thread.Sleep(rnd.Next(500, 3000)));

try
{
    Task.WaitAll(tasks);
    Console.WriteLine("Status of all tasks:");
    foreach (var t in tasks)
        Console.WriteLine("   Task #{0}: {1}", t.Id, t.Status);
}
catch (AggregateException)
{
    Console.WriteLine("An exception occurred.");
}

其輸出結果如下:
在這裏插入圖片描述
可以看到所有任務都是已完成狀態。

3、Task.WaitAny

這個方法會返回一個索引值,指明完成的是哪一個Task對象。用法同Task.WaitAll,就是等待任何一個任務完成就繼續向下執行,將上面的代碼WaitAll替換爲WaitAny,輸出結果如下:
在這裏插入圖片描述

4、Task的取消

  在實際應用中,出現異常或者用戶點擊取消等等,我們就需要取消這個任務。那麼如何取消一個Task呢?我們通過Cancellation的tokens來取消一個Task。在很多Task的Body裏面包含循環,我們可以在輪詢的時候判斷IsCancellationRequested屬性是否爲True,如果是True的話可以使用以下選項之一終止操作:
  1)簡單地從委託中返回。 在許多情況下,這樣已足夠;但是,採用這種方式取消的任務實例會轉換爲 TaskStatus.RanToCompletion 狀態,而不是 TaskStatus.Canceled狀態。
  2)引發 OperationCanceledException ,並將其傳遞到在其上請求了取消的標記。 完成此操作的首選方式是使用 ThrowIfCancellationRequested方法。 採用這種方式取消的任務會轉換爲 Canceled 狀態,調用代碼可使用該狀態來驗證任務是否響應了其取消請求。

如果你在等待轉換爲 Canceled 狀態的任務,則會引發 System.Threading.Tasks.TaskCanceledException 異常(包裝在AggregateException 異常中)。 請注意,此異常指示成功的取消,而不是有錯誤的情況。

下面來看一個例子:這裏開啓了一個Task,並給token註冊了一個方法,輸出一條信息,然後執行ReadKey開始等待用戶輸入,用戶點擊回車後,執行tokenSource.Cancel方法,取消任務。

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

var task = Task.Factory.StartNew(() =>
{
    for (var i = 0; i < 1000; i++)
    {
        System.Threading.Thread.Sleep(1000);
        if (token.IsCancellationRequested)
        {
            //在取消標誌引用的CancellationTokenSource上如果調用Cancel,就會拋出OperationCanceledException
            token.ThrowIfCancellationRequested();
            Console.WriteLine("Abort mission success!");
            return;
        }
    }
}, token);

//#region 註冊回調函數,當CancellationTokenSource.Cancel()執行後,調用回調函數
token.Register(() =>{ Console.WriteLine("此處回調函數");});

Console.WriteLine("Press enter to cancel task...");
Console.ReadKey();
tokenSource.Cancel();

try
{
    task.Wait();
}
catch (AggregateException ex)
{
    foreach (var e in ex.Flatten().InnerExceptions)
        Console.WriteLine("   {0}: {1}", e.GetType().Name, e.Message);
    //將任何OperationCanceledException對象都視爲已處理。其他任何異常都造成拋出一個AggregateException,其中只包含未處理的異常
    ex.Handle(e => e is OperationCanceledException);
}
finally
{
    Console.WriteLine("Status:{0},  Has Exceprion:{1} ", task.Status, task.Exception != null);
}

其輸出結果如下:
在這裏插入圖片描述

接下來註釋第12行代碼token.ThrowIfCancellationRequested();此行代碼運行結果如下:
在這裏插入圖片描述
可以看出,任務的狀態不是Canceled而是RanToCompletion

官網例子:下面的示例創建了10個任務,這些任務將循環,直到計數器的值遞增爲2000000。當前5個任務達到2000000時,取消標記將被取消,並且任何其計數器未達到2000000的任務都將被取消。然後,該示例檢查每個任務的 Status 屬性,以指示該任務是否已成功完成或已取消。 對於已完成的,它會顯示任務返回的值。

var tasks = new List<Task<int>>();
var source = new CancellationTokenSource();
var token = source.Token;
int completedIterations = 0;

for (int n = 0; n <= 9; n++)
    tasks.Add(Task.Run(() =>
    {
        int iterations = 0;
        for (int ctr = 1; ctr <= 2000000; ctr++)
        {
            token.ThrowIfCancellationRequested();
            iterations++;
        }
        Interlocked.Increment(ref completedIterations);
        if (completedIterations >= 5)
            source.Cancel();
        return iterations;
    }, token));

Console.WriteLine("Waiting for the first 10 tasks to complete...\n");
try
{
    Task.WaitAll(tasks.ToArray());
}
catch (AggregateException)
{
    Console.WriteLine("Status of tasks:\n");
    Console.WriteLine("{0,10} {1,20} {2,14:N0}", "Task Id", "Status", "Iterations");
    foreach (var t in tasks)
        Console.WriteLine("{0,10} {1,20} {2,14}", t.Id, t.Status, t.Status != TaskStatus.Canceled ? t.Result.ToString("N0") : "n/a");
}

運行結果如下:
在這裏插入圖片描述
有關詳細信息和示例,請參閱使用延續任務鏈接任務和如何:取消任務及其子級

5、Task.ContinueWith

創建一個在目標 Task 完成時異步執行的延續任務。在實際應用程序中,延續委託可能會記錄有關異常的詳細信息,並可能生成新任務以從異常中恢復。

  要寫可伸縮的軟件,一定不能使你的線程阻塞。這意味着如果調用Wait或者在任務未完成時查詢Result屬性,極有可能造成線程池創建一個新線程,這增大了資源的消耗,並損害了伸縮性。ContinueWith便是一個更好的方式,一個任務完成時它可以啓動另一個任務,實現Task的延續。使用 TaskContinuationOptions枚舉中的值,用於設置計劃延續任務的時間以及延續任務的工作方式的選項。 這包括條件(如 OnlyOnCanceled)和執行選項(如 ExecuteSynchronously)。
下面看代碼示例:

//使用了Task<TResult>(Func<Object,TResult>, Object)構造函數
var t = Task.Factory.StartNew(i => { return (int)i + 100; }, 10000);
t.ContinueWith(task => Console.WriteLine("The result is:{0}", task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
t.ContinueWith(task => Console.WriteLine("{0}: {1}", t.Exception.InnerException.GetType().Name, t.Exception.InnerException.Message), TaskContinuationOptions.OnlyOnFaulted);
t.ContinueWith(task => Console.WriteLine("cancel:" + task.IsCanceled), TaskContinuationOptions.OnlyOnCanceled);

運行結果:
在這裏插入圖片描述

6、Task<TResult>.Result

在一個Task運行完成後,可用Result屬性獲取結果值。訪問屬性的 get 訪問器會阻止調用線程, 直到異步操作完成;它等效於調用Wait方法。
操作結果可用後, 它將被存儲並在對屬性的Result後續調用後立即返回。請注意, 如果在任務的操作過程中發生異常, 或者如果任務已取消, 則Result屬性不會返回值。 相反, 嘗試訪問屬性值會引發AggregateException異常。

Task.Run(() => { return "One"; }).ContinueWith(ss => { Console.WriteLine(ss.Result); });

結果輸出:One

四、進階

1、Task的嵌套

Task中的嵌套分爲兩種,關聯嵌套和非關聯嵌套,就是說內層的Task(子任務)和外層的Task(父任務)是否有聯繫。默認情況下,子任務獨立於父任務。並且必須顯式指定 TaskCreationOptions.AttachedToParent 選項來創建附加子任務。

分離子任務:下面實例中子任務與父任務隨機先完成。

var parent = Task.Factory.StartNew(() => {
    Console.WriteLine("Outer task executing.");

    var child = Task.Factory.StartNew(() => {
        Console.WriteLine("Nested task starting.");
        Thread.SpinWait(500000);
        Console.WriteLine("Nested task completing.");
    });
});

parent.Wait();
Console.WriteLine("Outer has completed.");

運行結果:
在這裏插入圖片描述
附加子任務:不同於分離子任務,附加子任務與父任務緊密同步,當所有附加子任務完成後父任務纔會顯示完成。可以通過使用任務創建語句中的 TaskCreationOptions.AttachedToParent 選項,將之前示例中的分離子任務更改爲附加子任務,如以下示例中所示。

var parent = Task.Factory.StartNew(() => {
    Console.WriteLine("Parent task executing.");
    var child = Task.Factory.StartNew(() => {
        Console.WriteLine("Attached child starting.");
        Thread.SpinWait(5000000);
        Console.WriteLine("Attached child completing.");
    }, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
Console.WriteLine("Parent has completed.");

運行結果:
在這裏插入圖片描述

注意:如果父任務是通過調用 Task.Run 方法而創建的,則可以隱式阻止子任務附加到其中。使用TaskCreationOptions.LongRunning標記爲長時間運行的任務,則任務不會使用線程池,而在單獨的線程中運行。

2、Task異常處理

由在任務內部運行的用戶代碼引發的未處理異常會傳播回調用線程。如果使用靜態或實例 Task.Wait 方法之一,異常會傳播,異常處理方法爲將調用封閉到 try/catch 語句中。 如果任務是所附加子任務的父級,或在等待多個任務,那麼可能會引發多個異常。
  爲了將所有異常傳播回調用線程,任務基礎結構會將這些異常包裝在 AggregateException 實例中。 AggregateException 異常具有 InnerExceptions 屬性,可枚舉該屬性來檢查引發的所有原始異常,並單獨處理(或不處理)每個異常。 也可以使用 AggregateException.Handle 方法處理原始異常,篩選掉可視爲“已處理”的異常,而無需進一步使用任何邏輯。
例子:

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
tokenSource.Cancel();

Task[] tasks = new Task[2];
tasks[0] = Task.Run(() => { throw new NotImplementedException("Task Error!"); });
tasks[1] = Task.Run(() => { token.ThrowIfCancellationRequested(); }, token);

try
{
    Task.WaitAll(tasks);
}
catch (AggregateException ae)
{
    //將異常視爲“已處理”,而無需進一步使用任何邏輯。
    //ae.Handle(ex => { return true; });

    Console.WriteLine("One or more exceptions occurred:");
    foreach (var e in ae.InnerExceptions)
        Console.WriteLine("   {0}: {1}", e.GetType().Name, e.Message);
    Console.WriteLine("\nStatus of tasks:");
    foreach (var t in tasks)
    {
        Console.WriteLine("   Task #{0}: {1}", t.Id, t.Status);
        //通過使用 Task.Exception 屬性觀察異常。推薦使用僅在前面的任務出錯時才運行的延續任務觀察 Exception 屬性。
        if (t.Exception != null)
        {
            foreach (var ex in t.Exception.InnerExceptions)
                Console.WriteLine("      {0}: {1}", ex.GetType().Name, ex.Message);
        }
    }
}

運行結果:
在這裏插入圖片描述
附加任務異常處理:如果某個任務具有引發異常的附加子任務,則會在將該異常傳播到父任務之前將其包裝在 AggregateException 中,父任務將該異常包裝在自己的 AggregateException 中,然後再將其傳播回調用線程。 在這種情況下,在 Task.Wait、WaitAny、或 WaitAll 方法處捕獲的 AggregateException 異常的 InnerExceptions 屬性包含一個或多個 AggregateException 實例,而不包含導致錯誤的原始異常。

var task1 = Task.Factory.StartNew(() =>
{
    var child1 = Task.Factory.StartNew(() =>
    {
        var child2 = Task.Factory.StartNew(() =>
        {
            // This exception is nested inside three AggregateExceptions.
            throw new NotImplementedException("Attached child2 faulted.");
        }, TaskCreationOptions.AttachedToParent);

        // This exception is nested inside two AggregateExceptions.
        throw new ArgumentException("Attached child1 faulted.");
    }, TaskCreationOptions.AttachedToParent);
});
try
{
    task1.Wait();
}
catch (AggregateException ae)
{
    foreach (var e in ae.Flatten().InnerExceptions)
        Console.WriteLine("   {0}: {1}", e.GetType().Name, e.Message);
}

運行結果:
在這裏插入圖片描述
分離任務異常處理:默認情況下,子任務在創建時處於分離狀態。必須在直接父任務中處理或重新引發從分離任務引發的異常;將不會採用與附加子任務傳播回異常相同的方式將這些異常傳播回調用線程。 最頂層的父級可以手動重新引發分離子級中的異常,以使其包裝在 AggregateException 中並傳播回調用線程。

var task1 = Task.Run(() =>
{
    var nested1 = Task.Run(() => { throw new ArgumentException("Detached child task faulted."); });
    // Here the exception will be escalated back to the calling thread.We could use try/catch here to prevent that.
    nested1.Wait();
});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{
    foreach (var e in ae.Flatten().InnerExceptions)
        Console.WriteLine("   {0}: {1}", e.GetType().Name, e.Message);
}

運行結果:
在這裏插入圖片描述

3、TaskFactory(任務工廠)

一個工廠對象,可創建多種 TaskTask<TResult> 對象。靜態Factory屬性返回的默認TaskFactory類的默認實例。
下面的示例使用靜態Factory屬性以使兩個調用TaskFactory<TResult>.StartNew方法。兩個任務分別返回一個隨機值。然後,它調用TaskFactory.ContinueWhenAll(Task[], Action<Task[]>)方法,顯示已完成後其中最大值。

Random rnd = new Random();
Task<int>[] tasks = new Task<int>[2];
tasks[0] = Task.Factory.StartNew(() => rnd.Next(1, 100));
tasks[1] = Task.Factory.StartNew(() => rnd.Next(1, 100));

Task.Factory.ContinueWhenAll(tasks, completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), CancellationToken.None)
    .ContinueWith(t => Console.WriteLine("The maxinum is: " + t.Result), TaskContinuationOptions.ExecuteSynchronously).Wait(); // Wait用於測試;

您還可以調用之一TaskFactory<TResult>類構造函數來配置Task<TResult>對象的TaskFactory<TResult>類創建。下面的示例配置一個新TaskFactory<TResult>對象來創建具有指定的取消標記、 任務創建選項、 延續選項和自定義的任務計劃程序的任務。

CancellationTokenSource cts = new CancellationTokenSource();
TaskFactory<int> factory = new TaskFactory<int>(cts.Token, TaskCreationOptions.PreferFairness, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
var t1 = factory.StartNew(() => { Console.WriteLine("Task={0},Thread={1}", Task.CurrentId, Thread.CurrentThread.ManagedThreadId); return 0; });
var t2 = factory.StartNew(() => { Console.WriteLine("Task={0},Thread={1}", Task.CurrentId, Thread.CurrentThread.ManagedThreadId); return 0; });
cts.Dispose();

在大多數情況下,您不需要實例化一個新TaskFactory<TResult>實例。相反,可以使用靜態Task<TResult>.Factory屬性,它返回一個工廠對象,將使用默認值。然後可以調用其方法來啓動新任務或定義任務延續。

五、綜合實例

實例1:體現多任務如何協作。如圖:

得到3
得到4
得到7
任務1,返回結果1
任務2,返回任務1的結果加上2
任務3,返回任務1的結果加上3
任務4,返回任務2的結果加上4
任務完成,輸出結果11

實現代碼:

var task = Task.Run(() =>
{
    var t1 = Task.Run(() => { Console.WriteLine("Task 1 running..."); return 1; });
    var t2 = Task.Run(() => { Console.WriteLine("Task 2 running..."); return t1.Result + 2; });
    var t3 = Task.Run(() => { Console.WriteLine("Task 3 running..."); return t1.Result + 3; });
    var t4 = Task.Run(() => { Console.WriteLine("Task 4 running..."); return t2.Result + 4; });
    Console.WriteLine("Task Finished! The result is {0}", t3.Result + t4.Result);
});

運行結果:
在這裏插入圖片描述

參考文章

5天玩轉C#並行和多線程編程
C#線程篇—Task(任務)和線程池不得不說的祕密(5)

發佈了57 篇原創文章 · 獲贊 67 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章