簡介.NET 4.0的多工執行利器--Task
20 |
前陣子試寫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的基礎概念之一,值得大家花點時間熟悉,有益無害。
Comments
# by bill chung
TPL,讚
# by Edward
黑大您好,跟您請教一個問題, 我使用interop.adodb 與 interop.cdo來載入.eml的檔案, 以讀取.eml的相關資訊, 不知道是否自己程式的寫法有問題, 在執行loadfromfile的函數時, 會出現「存放空間不足,無法完成此操作」0x8007000E。 共有五萬多個.eml檔案, 但是讀到第兩千~七千的某筆時(發生時間不一定), 就會出現以上錯誤。 我在每讀一個檔案後,會把宣告的物件設為null, 要讀下一個檔案時,會重新new一個物件。 以上,煩請指導,謝謝。
# by Jeffrey
to Edward, 直覺是CDO的問題(or Bug?)導致類似Memory Leak的狀況。記憶體管理問題出錯,錯誤發生的筆數不固定倒也合理,要追查可能不太容易! 或許可以考量換用其他元件或程式庫處理EML。
# by Edward
黑大您好, 中斷點是停在adodb.stream.loasfromfile, 還沒執行到cdo的部份, 當然也可能是前幾次cdo造成的吧。 還有一個奇怪的問題, 相同的程式,在windows 2003 r2上執行會有這個bug, 但在win xp卻沒有, 這又可能是什麼原因呢? 以上,煩請指導,謝謝。
# by Edward
黑大您好: 不知道您是否有建議的元件或函式庫可以處理.eml的檔案, 能讀取mail相關資訊(附件、to、from……) 謝謝。
# by Edward
黑大您好神,我把cdo相關程式註解後測試,就不會有錯誤了。 但是為什麼xp不會出錯呢?令人費解。
# by Edward
黑大您好: 加上GC.Collect後,問題已解決, 只是不懂為什麼在win 2003才會有這個記憶體問題, 而win xp不會, 不知道黑大您認為可能的原因是什麼。
# by Jeffrey
to Edward, 我的猜測是兩台機器上的CDO版本有所差異導致不同結果,安裝Outlook或Exchange Server時也可能去更動CDO版本。除此之外,Windows或其他環境參數也可能會有影響,實務上同樣的OS,一台OK一台不行的狀況也不算罕見,要繼續追下去還得耗一番功夫,動用到進階工具,很難以目前蒐集的資訊論定問題根源。
# by Edward
了解了,謝謝黑大的指導。
# by Edward
黑大您好: 請教您一個問題, 我有一個批次檔,內容是重覆呼叫某個執行檔,但參數不同,例如: a.exe 1 2 3 a.exe 4 5 6 a.exe 7 8 9 想請教的是,執行這類的批次檔,是否可以使用此討論的task, 若是可以,可以請教大概的寫法嗎??
# by Jeffrey
to Edward,你可以考慮用.NET程式取代批次檔呼叫a.exe傳入不同參數(範例:http://blog.darkthread.net/post-2007-08-17-tips-excute-exe-and-get-its-output-from-net.aspx ),不知是否符合你的需求。
# by Edward
黑大您好: 您說的方法我知道, 我想請教的事, 如果我把要執行的多個a.exe放在一個文字檔, 是否可以利用 task 一次讀取這個文字檔中多個a.exe來執行。 謝謝
# by Jeffrey
to Edward, 不是很懂「利用 task 一次讀取這個文字檔中多個a.exe來執行」。意思是「寫一個批次檔run.bat,裡面有兩行 a.exe 1 a.exe 2 .NET程式將其當成文字檔讀入,用Task同時啟動兩個a.exe Process,一個傳入參數1,一個傳入參數2」嗎?
# by Edward
黑大,不好意思,表達能力不好,就是您理解的那樣。 目前我是有一個批次檔,裡面有上百個指令如下 a.exe 1 a.exe 2 a.exe 3 . . 批次檔是一行執行完才執行下一行, 因此想說是不是能利用.net 的 task 同時讀取多行一併執行
# by Jeffrey
to Edward, 理論上是可行的。但必須衡量某些情境下執行的Process過多,反而會造成效能下降。一般來說,若a.exe為單緒且執行需耗用大量CPU,則最適同時執行的數量等於CPU核數;若a.exe執行涉及大量本機硬碟讀寫,同時執行個數過多會造成磁碟讀寫頭切換頻繁,反而效能下降。若a.exe要等待網路回應,則是最適合增加同時執行個數的情境(但要小心不要壓垮遠端的伺服器)。 因此,這類情境我會先決定要同時執行的Process個數(例如:8個),先將上百個指令轉作工作資料放進Queue裡,開啟8個Task,每個Task由Queue中取出一個工作,執行完畢後再取下一個工作,可想像成8個作業員自己去籃子裡拿出材料加工,當Queue中沒有工作後,Task再自行結束。 實作細節推薦參考安德魯的系列文章:http://columns.chicken-house.net/columns/post/2007/12/14/ThreadPool-e5afa6e4bd9c-1-e59fbae69cace6a682e5bfb5.aspx
# by Edward
黑大,謝謝您,我的是屬於要等待網路回應的那類, 我會好好參考安德魯的文章
# by 烏龍茶
Hi~ 我之前是寫C 想學習C# 但我看不懂這寫法@@ () =>{} Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("Done!"); }); 是否能解釋一下呢? 謝謝
# by Jeffrey
to 烏龍茶,這是 C# 3.0 加入的 Lambda 寫法 ,參考:https://msdn.microsoft.com/zh-tw/library/bb397687.aspx
# by Saint
黑大新年快樂,安德魯的連結失效,可否提供關鍵字方便查詢,剛好過完年也需要修改一隻批次程式,情景和前位網友相同,祝你豬事順利
# by saint
應該是這個文章 https://columns.chicken-house.net/2007/12/14/threadpool-%E5%AF%A6%E4%BD%9C-1-%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/ 他加入了HTTPS,謝謝黑大~