標籤:
隨著 .NET 4.0的到來,她與以前各版本的一個明顯差別就是並行功能的增強,以此來適應這個多核的世界。於是引入了一個新概念---任務,作為支援並行運算的重要組成部分,同時,也作為對線程池的一個補充和完善。從所周知,使用線程池有兩個明顯的缺點,那就是一旦把我們要執行的任務放進去後,什麼時候執行完成,以及執行完成後需要傳回值,我們都無法通過內建的方式而得知。由於任務(Task)的推出,使得我們對並行編程變得簡單,而且不用關心底層是怎麼實現的,由於比線程池更靈活,如果能掌握好Task,對於寫出高效的並行代碼非常有協助。
一、建立任務
在System.Threading.Tasks命名空間下,有兩個新類,Task及其泛型版本Task<TResult>,這兩個類是用來建立任務的,如果執行的代碼不需要傳回值,請使用Task,若需要傳回值,請使用Task<TResult>。
建立任務的方式有兩種,一種是通過Task.Factory.StartNew方法來建立一個新任務,如:
Task task = Task.Facotry.StartNew(()=>Console.WriteLine(“Hello, World!”));//此行代碼執行後,任務就開始執行
另一種方法是通過Task類的建構函式來建立一個新任務,如:
Task task = new Task(()=>Console.WriteLine(“Hello, World!”));//此處只把要完成的工作交給任務,但任務並未開始
task.Start();//調用Start方法後,任務才會在將來某個時候開始執行。
同時,我們可以調用Wait方法來等待任務的完成或者調用IsCompleted屬性來判斷任務是否完成。需要說明的是,兩種建立任務的方法都可以配合TaskCreationOptions枚舉來實現我們對任務執行的行為具體控制, 同時,這兩種建立方式允許我們傳遞一個TaskCreationOptions對象來取消正在運行中的任務,請看任務的取消。
二、任務的取消
這世界唯一不變的就是變化,當外部條件發生變化時,我們可能會取消正在執行的任務。對於.NET 4.0之前,.NET並未提供一個內建的解決方案來取消線程池中正在執行的代碼,但在.NET 4.0中,我們有了Cooperative Cancellation模式,這使得取消正在執行的任務變得非常簡單。如下所示:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskDemo
{
class Program
{
static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
Task t = new Task(() => LongRunTask(cts.Token));
t.Start();
Thread.Sleep(2000);
cts.Cancel();
Console.Read();
}
static void LongRunTask(CancellationToken token)
{
//此處方法類比一個耗時的工作
for (int i = 0; i < 1000; i++)
{
if (!token.IsCancellationRequested)
{
Thread.Sleep(500);
Console.Write(".");
}
else
{
Console.WriteLine("任務取消");
break;
}
}
}
}
}
三、任務的異常機制
在任務執行過程中產生的未處理異常,任務會把它暫時隱藏起來,裝進一個集合中。當我們調用Wait方法或者Result屬性時,任務會拋出一個AggregateException異常。我們可以通過調用AggregateException對象的唯讀屬性InnerExceptions來得到一個ReadOnlyCollection<Exception>對象,它才是儲存拋出異常的集合,它的第一個元素就是最初拋出的異常。同樣的,AggregateException對象的InnerException屬性也會返回最初拋出的異常。
值得重視的是,由於任務的隱藏機制的特點,一旦產生異常後,如果我們不調用相應的方法或者屬性查看異常,我們也無法判斷是否有異常產生(Task不會主動拋出異常)。當Task對象被GC回收時,Finalize方法會查檢是否有未處理的異常,如果不幸剛才好有,則Finalize方法會將此AggregateException再度拋出,如果再不幸,我們沒有捕獲處理這個異常,則我們的程式會立即中止運行。如果發生這樣的事情,會是多麼大的災難啊!
為了避免這種不幸的發生,我們可以通過註冊TaskScheduler類的靜態UnobservedTaskException事件來處理這種未被處理的異常,避免程式的崩潰。
四、任務啟動任務
任務的強大與靈活之一是,當我們完成一個任務時,可以自動開始一個新任務的執行。如下所示:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskDemo
{
public class AutoTask
{
static void Main()
{
Task task = new Task(() => { Thread.Sleep(5000); Console.WriteLine("Hello,"); Thread.Sleep(5000); });
task.Start();
Task newTask = task.ContinueWith(t => Console.WriteLine("World!"));
Console.Read();
}
}
}
對於ContinueWith方法,我們可以配合TaskContinuationOptions枚舉,得到更多我們想要的行為。
五、子任務
任務是支援父子關係的,即在一個任務中建立新任務。如下所示:
using System;
using System.Threading.Tasks;
namespace TaskDemo
{
class ChildTask
{
static void Main()
{
Task parant = new Task(() =>
{
new Task(() => Console.WriteLine("Hello")).Start();
new Task(() => Console.WriteLine(",")).Start();
new Task(() => Console.WriteLine("World")).Start();
new Task(() => Console.WriteLine("!")).Start();
});
parant.Start();
Console.ReadLine();
}
}
}
值得注意的是,以上代碼中所示的子任務的調用並不是以代碼的出現先後為順序來調用的。
六、任務工廠
在某些情況下,我們會遇到建立大量的任務,而恰好這些任務共用某個狀態參數(如CancellationToken),為了避免大量的調用任務的構造器和一次又一次的參數傳遞,我們可以使用任務工廠來為我們處理這種大量建立工作。如下代碼所示:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskDemo
{
public class FactoryOfTask
{
static void Main()
{
Task parent = new Task(() =>
{
CancellationTokenSource cts = new CancellationTokenSource();
TaskFactory tf = new TaskFactory(cts.Token);
var childTask = new[]
{
tf.StartNew(()=>ConcreteTask(cts.Token)),
tf.StartNew(()=>ConcreteTask(cts.Token)),
tf.StartNew(()=>ConcreteTask(cts.Token))
};
Thread.Sleep(5000);//此處睡眠等任務開始一定時間後才取消任務
cts.Cancel();
}
);
parent.Start();//開始執行任務
Console.Read();
}
static void ConcreteTask(CancellationToken token)
{
while (true)
{
if (!token.IsCancellationRequested)
{
Thread.Sleep(500);
Console.Write(".");
}
else
{
Console.WriteLine("任務取消");
break;
}
}
}
}
}
七、任務發送器
任務的調度通過發送器來實現的,目前,.NET 4.0內建兩種任務發送器:線程池任務發送器(thread pool task scheduler)和同步上下文任務發送器(synchronization context task scheduler)。預設情況下,應用程式使用線程池任務發送器調用線程池的背景工作執行緒來完成任務,如受計算限制的非同步作業。同步上下文任務發送器通常使用UI線程來完成與Windows Forms,Windows Presentation Foundation(WPF)以及SilverLight應用程式相關的任務。
可喜的是,.NET 4.0 提供了TaskScheduler抽象類別供開發人員繼承來實現自訂任務發送器的開發,有興趣的同學可以試試。
八、總結
任務給了我們更多的方便性、靈活性的同時,也帶來了比線程池更多的資源消耗。如果想減少資源消耗,請直接使用線程池QueueUserWorkItem方法效果會更好;如果想要更多的控制與靈活性,任務(Task)是不二的選擇。這個要我們開發人員自己去斟酌了。
參考文獻:《CLR Via C#》,Third edtion, Jeffrey Richer,726頁-739頁
《Introducing .NET 4.0 With Visual Studio 2010》,Alex Mackey,106頁-111頁
http://www.cnblogs.com/myshell/archive/2010/03/23/1692059.html
.NET 4.0 任務(Task)