簡介.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的基礎概念之一,值得大家花點時間熟悉,有益無害。

歡迎推文分享:
Published 20 July 2012 06:00 AM 由 Jeffrey
Filed under:
Views: 64,452



意見

# bill chung said on 19 July, 2012 10:06 PM

TPL,讚

# Edward said on 07 May, 2013 11:21 PM

黑大您好,跟您請教一個問題,

我使用interop.adodb 與 interop.cdo來載入.eml的檔案,

以讀取.eml的相關資訊,

不知道是否自己程式的寫法有問題,

在執行loadfromfile的函數時,

會出現「存放空間不足,無法完成此操作」0x8007000E。

共有五萬多個.eml檔案,

但是讀到第兩千~七千的某筆時(發生時間不一定),

就會出現以上錯誤。

我在每讀一個檔案後,會把宣告的物件設為null,

要讀下一個檔案時,會重新new一個物件。

以上,煩請指導,謝謝。

# Jeffrey said on 08 May, 2013 01:48 AM

to Edward, 直覺是CDO的問題(or Bug?)導致類似Memory Leak的狀況。記憶體管理問題出錯,錯誤發生的筆數不固定倒也合理,要追查可能不太容易! 或許可以考量換用其他元件或程式庫處理EML。

# Edward said on 08 May, 2013 02:09 AM

黑大您好,

中斷點是停在adodb.stream.loasfromfile,

還沒執行到cdo的部份,

當然也可能是前幾次cdo造成的吧。

還有一個奇怪的問題,

相同的程式,在windows 2003 r2上執行會有這個bug,

但在win xp卻沒有,

這又可能是什麼原因呢?

以上,煩請指導,謝謝。

# Edward said on 08 May, 2013 02:11 AM

黑大您好:

不知道您是否有建議的元件或函式庫可以處理.eml的檔案,

能讀取mail相關資訊(附件、to、from……)

謝謝。

# Edward said on 08 May, 2013 02:32 AM

黑大您好神,我把cdo相關程式註解後測試,就不會有錯誤了。

但是為什麼xp不會出錯呢?令人費解。

# Edward said on 08 May, 2013 11:01 PM

黑大您好:

加上GC.Collect後,問題已解決,

只是不懂為什麼在win 2003才會有這個記憶體問題,

而win xp不會,

不知道黑大您認為可能的原因是什麼。

# Jeffrey said on 09 May, 2013 12:10 AM

to Edward, 我的猜測是兩台機器上的CDO版本有所差異導致不同結果,安裝Outlook或Exchange Server時也可能去更動CDO版本。除此之外,Windows或其他環境參數也可能會有影響,實務上同樣的OS,一台OK一台不行的狀況也不算罕見,要繼續追下去還得耗一番功夫,動用到進階工具,很難以目前蒐集的資訊論定問題根源。

# Edward said on 09 May, 2013 04:17 AM

了解了,謝謝黑大的指導。

# Edward said on 01 September, 2014 02:47 AM

黑大您好:

請教您一個問題,

我有一個批次檔,內容是重覆呼叫某個執行檔,但參數不同,例如:

a.exe 1 2 3

a.exe 4 5 6

a.exe 7 8 9

想請教的是,執行這類的批次檔,是否可以使用此討論的task,

若是可以,可以請教大概的寫法嗎??

# Jeffrey said on 01 September, 2014 05:54 AM

to Edward,你可以考慮用.NET程式取代批次檔呼叫a.exe傳入不同參數(範例:blog.darkthread.net/post-2007-08-17-tips-excute-exe-and-get-its-output-from-net.aspx ),不知是否符合你的需求。

# Edward said on 03 September, 2014 11:58 PM

黑大您好:

您說的方法我知道,

我想請教的事,

如果我把要執行的多個a.exe放在一個文字檔,

是否可以利用 task 一次讀取這個文字檔中多個a.exe來執行。

謝謝

# Jeffrey said on 04 September, 2014 01:42 AM

to Edward, 不是很懂「利用 task 一次讀取這個文字檔中多個a.exe來執行」。意思是「寫一個批次檔run.bat,裡面有兩行

a.exe 1

a.exe 2

.NET程式將其當成文字檔讀入,用Task同時啟動兩個a.exe Process,一個傳入參數1,一個傳入參數2」嗎?

# Edward said on 04 September, 2014 01:49 AM

黑大,不好意思,表達能力不好,就是您理解的那樣。

目前我是有一個批次檔,裡面有上百個指令如下

a.exe 1

a.exe 2

a.exe 3

.

.

批次檔是一行執行完才執行下一行,

因此想說是不是能利用.net 的 task 同時讀取多行一併執行

# Jeffrey said on 04 September, 2014 02:31 AM

to Edward, 理論上是可行的。但必須衡量某些情境下執行的Process過多,反而會造成效能下降。一般來說,若a.exe為單緒且執行需耗用大量CPU,則最適同時執行的數量等於CPU核數;若a.exe執行涉及大量本機硬碟讀寫,同時執行個數過多會造成磁碟讀寫頭切換頻繁,反而效能下降。若a.exe要等待網路回應,則是最適合增加同時執行個數的情境(但要小心不要壓垮遠端的伺服器)。

因此,這類情境我會先決定要同時執行的Process個數(例如:8個),先將上百個指令轉作工作資料放進Queue裡,開啟8個Task,每個Task由Queue中取出一個工作,執行完畢後再取下一個工作,可想像成8個作業員自己去籃子裡拿出材料加工,當Queue中沒有工作後,Task再自行結束。

實作細節推薦參考安德魯的系列文章:columns.chicken-house.net/.../ThreadPool-e5afa6e4bd9c-1-e59fbae69cace6a682e5bfb5.aspx

# Edward said on 04 September, 2014 02:45 AM

黑大,謝謝您,我的是屬於要等待網路回應的那類,

我會好好參考安德魯的文章

# 烏龍茶 said on 14 April, 2017 12:30 PM

Hi~

我之前是寫C

想學習C#

但我看不懂這寫法@@

() =>{}

           Task.Factory.StartNew(() =>

           {

               Thread.Sleep(1000);

               Console.WriteLine("Done!");

           });

是否能解釋一下呢?

謝謝

# Jeffrey said on 14 April, 2017 08:54 PM

to 烏龍茶,這是 C# 3.0 加入的 Lambda 寫法 ,參考:msdn.microsoft.com/.../bb397687.aspx

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<July 2012>
SunMonTueWedThuFriSat
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication