Task及其異常處理的若干事項

來源:互聯網
上載者:User
Task簡述

微軟在.NET Framework 4.0的時候引進了一個新的類:System.Thread.Task,用它來表示一個非同步作業,比如從網上下載一個檔案,或者一個比較耗時的檔案寫入動作,如果把這些操作放到UI線程裡做,就引起使用者介面失去響應,所以我們要另外開線程去做這些事情,開線程並不是什麼新鮮事,只不過到了.NET Framework 4.0,微軟用Task對它進行了一次再封裝而已,這個再封裝帶來的直接好處當然是代碼更加簡單了,除此之外,還會有更好的效能,因為它的底層實現用了“線程池”,需要執行任務的時候,喚醒背景工作執行緒,否則讓背景工作執行緒轉入睡眠,這樣就省去了建立線程的開銷,而這一切對我們程式員來說可以是透明的,我們用就是了,微軟相當於是把Thread抽象成了Task。

最簡單的例子

好吧,先來一個最簡單的例子。

using System;using System.Threading;using System.Threading.Tasks;class Program{    static void Main(string[] args)    {        Task taskA = Task.Factory.StartNew(() => DoSomeWork(2000));        taskA.Wait(); //主線程阻塞在這裡,直到A任務完成        Console.WriteLine("taskA has completed.");     }        static void DoSomeWork(int val)    {         Thread.Sleep(val);//用Sleep來類比一個很耗時的任務    }}

代碼應該沒有太多需要解釋的地方,如果你對Lambda運算式不熟悉的話,看到“=>”這種符號可能會覺得有些奇怪,關於Lambda運算式,網上有很多文章,當然了,最推薦的還是微軟官方的文章《Lambda運算式(C#編程指南)》,有空的話看看,其實也花不了多少時間的,如果沒空,那我就在這裡簡單說說:它其實就是一個匿名函數。上面的代碼可以寫成這樣:

    static void ToDoTwoSecondsWork()    {        DoSomeWork(2000);    }    static void Main(string[] args)    {        Task taskA = Task.Factory.StartNew(ToDoTwoSecondsWork);        taskA.Wait(); //主線程阻塞在這裡,直到A任務完成        Console.WriteLine("taskA has completed.");    }

效果一樣的,但這樣寫要多寫一個叫“ToDoTwoSecondsWork”的函數,這樣顯式地寫函數名出來也是有好處的,就是方便設斷點調試。

提示:Wait方法可設定逾時等待時間,時間一到,即便任務未完成,也會返回。

ContinueWith

在上面的例子中,用了Wait方法來等待任務結束,這種方法其實還是會阻塞主線程的,也就是說會帶來UI失去響應,那這不符合我們的初衷啊,有什麼辦法?可以試試看這個:ContinueWith,ContinueWith並不阻塞主線程,主線程會繼續往下走,而背景工作執行緒把任務完成後,會調用一個回呼函數。

    static void Main(string[] args)    {        Task taskA = Task.Factory.StartNew(() => DoSomeWork(2000));        taskA.ContinueWith(t => Debug.WriteLine("taskA has completed."));        Console.ReadKey();    }

當任務A結束的時候,將會在Output視窗輸出"taskA has completed."這個訊息,為什麼我不直接在控制台上輸出呢?這是因為這個輸出動作的執行線程不是主線程,雖然這是一個控制台程式,但我們從設計上來說也不應該直接用背景工作執行緒去操作UI,背景工作執行緒和UI通訊的方式可以參考我另一篇部落格:《.net的Invoke》。

多個背景工作執行緒

上面的例子雖說也是“多線程”,但其實背景工作執行緒只有一個,我們有時候需要同時開啟多個背景工作執行緒,如“多線程下載”就是一個典型的例子,另外做一些特別耗時的多媒體處理工作時,我們也往往會把一個處理拆分為幾個部分,讓它們同時執行,充分利用現在的CPU的多核的優勢。下面看例子:

        Task[] tasks = new Task[10];        for (int i = 0; i < 10; i++)        {            tasks[i] = Task.Factory.StartNew(() => DoSomeWork(2000));        }        Task.WaitAll(tasks);        Console.WriteLine("All tasks have completed.");

我們一共建立了10個任務,也許你要問:這10個任務是同時執行的嗎?回答是否定的,.NET framework會自動安排它們執行,如果你的電腦有4個核心,很可能CPU會同時安排4個任務去執行,總之你不能假定這些任務是同時執行的,所以在分割任務的時候,最好任務和任務間彼此獨立,不要一個任務等待另一個任務,否則容易導致死結。

提示:如果你只需等待其中一個任務結束,你可以嘗試Task.WaitAny。

擷取結果

請看下面這個例子:

    static void Main(string[] args)    {        Task<int> taskA = Task<int>.Factory.StartNew(() => NumberSumWork(100));        taskA.Wait();        Console.WriteLine("The result is " + taskA.Result);     }         static int NumberSumWork(int iVal)    {        int iResult = 0;        for (int i = 1; i <= iVal; i++)        {            Thread.Sleep(40);            iResult += i;        }        return iResult;    }

其實擷取Result這個屬性本身就包含了Wait,所以上面的代碼也可以這樣寫:

    static void Main(string[] args)    {        Task<int> taskA = Task<int>.Factory.StartNew(() => NumberSumWork(100));        Console.WriteLine("The result is " + taskA.Result);     }

如果用ContinueWith的話,還可以這樣寫:

taskA.ContinueWith(t => Debug.WriteLine(t.Result));
拋出異常

我相信上面的內容都很好理解,但說到異常,這該如何處理呢?

先說個故事:前陣子我在做一個程式,這個程式有一個功能,那就是到網上去檢查最新版本,考慮到這個動作可能要消耗幾秒鐘的時間,我把它做成了非阻塞的形式,也就是用前面提到的那個“ContinueWith”,這樣使用者介面就可以一直不失去響應了,即使網路不通,那也是過了幾秒鐘之後,就會在介面上提醒使用者說“擷取最新版資訊失敗”。這個程式在我的電腦上跑沒有任何問題,但拿到了同事的電腦上就出現了問題,當網路不通的時候,程式也會“擷取最新版資訊失敗”,一旦有了這個失敗失敗,一做介面切換,程式就直接崩潰,在另一個同事那裡也出現了類似的問題,但崩潰的時間點不太一樣,我使用全域異常捕捉也捉不到這個異常,我經過大量調試,可在自己的電腦上就是無法重現這個問題,我確信這是由於使用了多線程引起的,但就是找不出原因,最後我把ContinueWith換掉,改成了阻塞的方式,就沒這個問題了,但這樣在執行更新檢查的時候介面也就會短暫地失去響應。後面我會再去分析這個事情,現在回到正題。

先來看最簡單的異常處理例子:

class MyException:Exception{    public MyException(string message)        :base(message)    {    }}class Program{    static void Main(string[] args)    {        Task taskA = Task.Factory.StartNew(()=>DoSomeWork(2000));        taskA.Wait();        Console.WriteLine("taskA has completed.");     }        static void DoSomeWork(int val)    {        Thread.Sleep(val);        throw new MyException("This is a test exception.");    }}

調試,出現異常了,執行點停留在Wait這個地方:

異常類型為AggregateException:

出錯了,異常類型不是Exception,而是AggregateException,為什麼呢?因為這是由別的線程產生的異常,並且可能不止一個異常,所以要用AggregateException這個類來充當一個異常的容器,背景工作執行緒產生的異常會放入這個容器中,最後由我們來處理這個容器。把上面的代碼改一改,就能產生一個包含多個Exception的AggregateException。

class Program{    static void Main(string[] args)    {        Task[] tasks = new Task[2];        tasks[0] = Task.Factory.StartNew(() => DoSomeWork(2000));        tasks[1] = Task.Factory.StartNew(() => DoSomeWork(3000));        Task.WaitAll(tasks);        Console.WriteLine("Tasks have completed.");     }        static void DoSomeWork(int val)    {        Thread.Sleep(val);        throw new MyException(string.Format("This is a test exception."+val));    }}

對於AggregateException的處理,可以這樣做:

        try        {            Task.WaitAll(tasks);        }        catch (AggregateException ae)        {            ae.Handle(x =>                {                    if (x is MyException)                    {                        Console.WriteLine("異常已經處理:"+x.Message);                        return true; //表示異常已經處理                    }                    return false; //表示異常未處理,繼續拋出                });        }

結果顯示為:
異常已經處理:This is a test exception.2000
異常已經處理:This is a test exception.3000

ContinueWith的異常

上文提到,ContinueWith是非阻塞的,try-catch它是沒辦法捕捉到AggregateException的,那這樣的話Task產生的異常都到哪裡去了?現在想想我前面講的那個故事:我使用ContinueWith的時候,一旦網路連接錯誤,程式在同事的電腦上就會在某個時候(比如介面切換的時候)崩潰,而在我的電腦上則無法重現這樣的問題。這樣說來:ContinueWith所產生的這個異常(網路連接異常)在我的電腦上被莫名其妙地忽略了,而在同事的電腦上卻被當作了未處理的異常。現在,我把我的那個程式簡化為下面的代碼(完整代碼):

using System;using System.Diagnostics;using System.Threading;using System.Threading.Tasks;class TestException:Exception{    public TestException(string message)        :base(message)    {    }}class Program{    static void Main(string[] args)    {        Task<int> taskA = Task<int>.Factory.StartNew(() => DoSomeWork(2000));        taskA.ContinueWith(t => Debug.WriteLine("taskA has completed. result is " + taskA.Result));          Console.ReadKey();        GC.Collect();        Console.ReadKey();    }         static int DoSomeWork(int val)    {        Thread.Sleep(val);        throw new TestException("This is a test exception.");        return val;    }}

運行這段代碼,什麼都不要動,等兩秒鐘,你會發現程式沒有報任何異常,雖然你能在Visual Studio的Output視窗中看到“A first chance exception of...”這樣的異常提示,但程式仍然是安然無恙的,在我的電腦上,再碰兩下斷行符號鍵,程式就退出了,沒有任何錯誤提示,而在同事的電腦上,等了這兩秒鐘後一碰斷行符號鍵,就出現了這樣的異常提示:

這是怎麼回事!這種問題自己是琢磨不出來的,只能到網上去尋找答案,請看這個連結:《Application Compatibility in the .NET Framework 4.5》

其中有以下的內容:

  • Feature: Unobserved exceptions in Task operations
  • Change: Because the Task class represents an asynchronous operation, it catches all non-severe exceptions that occur during asynchronous processing. In the .NET Framework 4.5, if an exception is not observed and your code never waits on the task, the exception will no longer propagate on the finalizer thread and crash the process during garbage collection.
  • Impact: This change enhances the reliability of applications that use the Task class to perform unobserved asynchronous processing. The previous behavior can be restored by providing an appropriate handler for the TaskScheduler.UnobservedTaskException event.

對於Task非同步作業中所產生的未被擷取的異常的處理,.NET Framework 4.5和.NET Framework 4.0是不一樣的,4.5會將它忽略掉,而4.0會很當真。這就是為什麼我的程式在我的電腦上重現不出那個錯誤的原因,我安裝了.NET Framework 4.5了,同事的電腦上還是4.0的。

也許你還有個問題:那為什麼要加入“GC.Collect()”這麼一個語句呢?你有沒有注意到?你運行這個程式的時候如果不動任何鍵,它是不報異常的,動一下才報,這是因為這樣的AggregateException是在執行記憶體回收的時候才會被拋出的,這就是為什麼我那個程式在同事的電腦上是在“介面切換”的時候才崩潰,因為那個時候很可能(並不一定)會執行一次記憶體回收,所以崩潰的時間點看起來也並不那麼固定,因為記憶體回收其實是由.NET Framework自動執行的,我們不應該去幹預它。

那如果我們要處理這個“被忽略”的異常,應該怎麼做呢?上面的例子改一下:

        taskA.ContinueWith(t =>                           {                               if (!t.IsFaulted)                               {                                   Debug.WriteLine("taskA has completed. result is " + taskA.Result);                               }                               else                               {                                   Debug.WriteLine("發生異常:" + t.Exception);    //Log異常                                   t.Exception.Handle(x => true);                  //並將異常標記為已處理                               }                           });

這樣一來,在.NET Framework 4.0中也不會出現異常了,為了相容.NET Framework 4.0,你得用這種寫法。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.