簡介.NET 4.0的多工執行利器--Task
前陣子試寫SignalR時,學到.NET 4.0在多工執行上提供了新類別--Task。初試之下,發現用它取代傳統Thread、ThreadPool寫法,能大幅簡化同步邏輯的寫法,頗為便利。整理幾個範例展示Task的使用方式,分享兼備忘。
先從最簡單的開始。test1()用以另一條Thread執行Thread.Sleep()及Console.WriteLine(),效果與ThreadPool.QueueUserWorkItem()相當。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
namespace TaskLab
{
class Program
{
static void Main(string[] args)
{
test1();
Console.Read();
}
static void test1()
{
//Task可以代替TheadPool.QueueUserWorkItem使用
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Done!");
});
Console.WriteLine("Async Run...");
}
}
}
StartNew()完會立刻執行下一行,故會先看到Aync Run,1秒後印出Done。
Async Run...
Done!
同時啟動數個作業多工並行,但要等待各作業完成再繼續下一步是常見的應用情境,傳統上可透過WaitHandle、AutoResetEvent、ManualResetEvent等機制實現;Task的寫法相對簡單,建立多個Task物件,再當成Task.WaitAny()或Task.WaitAll()的參數就搞定囉!
static void test2()
{
var task1 = Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
Console.WriteLine("Done!(3s)");
});
var task2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
Console.WriteLine("Done!(5s)");
});
//等待任一作業完成後繼續
Task.WaitAny(task1, task2);
Console.WriteLine("WaitAny Passed");
//等待兩項作業都完成才會繼續執行
Task.WaitAll(task1, task2);
Console.WriteLine("WaitAll Passed");
}
task1耗時3秒、task2耗時5秒,所以3秒後WaitAny()執行完成、5秒後WaitAll()執行完畢。
Done!(3s)
WaitAny Passed
Done!(5s)
WaitAll Passed
如果要等待多工作業傳回結果,透過StartNew<T>()指定傳回型別建立作業,隨後以Task.Result取值,不用額外寫Code就能確保多工作業執行完成後才讀取結果繼續運算。
static void test3()
{
var task = Task.Factory.StartNew<string>(() =>
{
Thread.Sleep(2000);
return "Done!";
});
//使用馬錶計時
Stopwatch sw = new Stopwatch();
sw.Start();
//讀task.Result時,會等到作業完畢傳回值後才繼續
Console.WriteLine("{0}", task.Result);
sw.Stop();
//要取得task.Result耗時約2秒
Console.WriteLine("Duration: {0:N0}ms", sw.ElapsedMilliseconds);
}
實際執行,要花兩秒才能跑完Console.WriteLine("{0}", task.Result),其長度就是Task執行並傳回結果的時間。
Done!
Duration: 2,046ms
如果要安排多工作業完成後接連執行另一段程式,可使用ContinueWith():
static void test4()
{
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Done!");
}).ContinueWith(task =>
{
//ContinueWith會等待前項工作完成才執行
Console.WriteLine("In ContinueWith");
});
Console.WriteLine("Async Run...");
}
如預期,ContinueWith()裡的程式會在Task完成後才被執行。
Async Run...
Done!
In ContinueWith
.ContinueWith()傳回值仍是Task物件,所以我們可以跟jQuery一樣玩接接樂,在ContinueWith()後方再接上另一個ContinueWith(),各段邏輯便會依順序執行。
static void test5()
{
//ContinueWith()可以串接
Task.Factory.StartNew(() =>
{
Thread.Sleep(2000);
Console.WriteLine("{0:mm:ss}-Done", DateTime.Now);
})
.ContinueWith(task =>
{
Console.WriteLine("{0:mm:ss}-ContinueWith 1", DateTime.Now);
Thread.Sleep(2000);
})
.ContinueWith(task =>
{
Console.WriteLine("{0:mm:ss}-ContinueWith 2", DateTime.Now);
});
Console.WriteLine("{0:mm:ss}-Async Run...", DateTime.Now);
}
Task耗時兩秒,第一個ContinueWith()耗時2秒,最後一個ContinueWith()接續在4秒後執行。
59:13-Async Run...
59:15-Done
59:15-ContinueWith 1
59:17-ContinueWith 2
最後一個例子比較複雜。ContinueWith()中的Action<Task>都會有一個輸入參數,藉以得知前一Task的執行狀態,有IsCompleted, IsCanceled, IsFaulted幾個屬性可用。
要取消執行,得借助CancellationTokenSource及其所屬CancellationToken類別,做法是在Task中持續呼叫CancellationToken.ThrowIfCancellationRequested(),一旦外部呼叫CancellationTokenSource.Cancel(),便會觸發OperationCanceledException,Task有機制偵測此種例外狀況,將結束作業執行後續的ContinueWith(),並指定Task.IsCanceled為True以為識別;而當Task程式發生Exception,也會結束作業觸發ContinueWith(),此時則Task.IsFaulted為True,ContinueWith()中可透過Task.Exception.InnerExceptions取得錯誤細節。
以下程式同時可測試Task正常、取消及錯誤三種情境,使用者透過輸入1,2或3來決定要測試哪一種。在Task外先宣告一個CancellationTokenSource類別,將其中的Token屬性當成StartNew()的第二項參數,而Task中則保留最初的五秒可以取消,方法是每隔一秒呼叫一次CancellationToken.ThrowIfCancellationRequested(),當程式外部呼叫CancellationTokenSource.Cancel(),Task就會結束。5秒後若未取消,再依使用者決定的測試情境return結果或是抛出Exception。ContinueWith()則會檢查IsCanceled, IsFaulted等旗標,並輸出結果。
static void test6()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken cancelToken = cts.Token;
Console.Write("Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : ");
var key = Console.ReadKey(); Console.WriteLine();
Task.Factory.StartNew<string>(() =>
{
//保留5秒偵測是否要Cancel
for (var i = 0; i < 5; i++)
{
Thread.Sleep(1000);
//如cancelToken.IsCancellationRequested
//抛出OperationCanceledException
cancelToken.ThrowIfCancellationRequested();
}
switch (key.Key)
{
case ConsoleKey.D1: //選1時
return "OK";
case ConsoleKey.D3: //選2時
throw new ApplicationException("MyException");
}
return "Unknown Input";
}, cancelToken).ContinueWith(task =>
{
Console.WriteLine("IsCompleted: {0} IsCanceled: {1} IsFaulted: {2}",
task.IsCompleted, task.IsCanceled, task.IsFaulted);
if (task.IsCanceled)
{
Console.WriteLine("Canceled!");
}
else if (task.IsFaulted)
{
Console.WriteLine("Faulted!");
foreach (Exception e in task.Exception.Flattern().InnerExceptions)
{
Console.WriteLine("Error: {0}", e.Message);
}
}
else if (task.IsCompleted)
{
Console.WriteLine("Completed! Result={0}", task.Result);
}
});
Console.WriteLine("Async Run...");
//如果要測Cancel,2秒後觸發CancellationTokenSource.Cancel
if (key.Key == ConsoleKey.D2)
{
Thread.Sleep(2000);
cts.Cancel();
}
}
以下是三種測試情境的結果。
正常執行:
Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : 1
Async Run...
IsCompleted: True IsCanceled: False IsFaulted: False
Completed! Result=OK
取消: (IsCanceled為True,但留意IsCompleted也算True)
Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : 2
Async Run...
IsCompleted: True IsCanceled: True IsFaulted: False
Canceled!
錯誤: (IsFaulted為True,IsCompleted也是True)
Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : 3
Async Run...
IsCompleted: True IsCanceled: False IsFaulted: True
Faulted!
Error: MyException
【小結】
說穿了,Task能做的事,過去使用Thread/ThreadPool配合Event、WaitHandle一樣能辦到,但使用Task能以較簡潔的語法完成相同工作,使用.NET 4.0開發多工作業程式應可多加利用。同時,Task也是.NET 4.5 async await的基礎概念之一,值得大家花點時間熟悉,有益無害。