ASP.NET4.5Web API及非同步程式開發系列(1)

Halower發表於2013-07-15

認識非同步程式開發設計模型

從VS2012開始引入的新的非同步程式設計的支援-------async/await設計模型

  1. 之前的當我們支援非同步作業的時候,往往使用多執行緒開解決,我們比較熟悉的就是
  2. 執行者:Thread,ThreadPool (執行緒和執行緒池,後者有利於資源的有效利用)
  3. 非同步的設計模型:Begin方法/End方法,Async事件/Completed事件(主要是非同步委託之類的,我在我以前的博文中有寫過專題)
  4. BackgroundWorker控制項
  5. Task Parallel Library

  雖然今天的重點是.NET4.5的async/await設計模式但是由於很多人對於.NET4.0中的Task仍然還是沒有接觸過,Task也是.NET 4.5 async await的基礎概念之一,值得大家花點時間熟悉,那麼這裡就將他也作為一個附加專題來做一下講解。

附屬專題:.NET4.0多執行緒開發之利器---àTask

到我們在開發SignalR程式的時候,就必須要使用到多執行緒,假設沒有.NET4.5的支援,那麼你可能想到的最簡單方式就是使用Task,它取代了傳統的Thread,TheadPool的寫法,能大幅度的簡化同步邏輯的寫法,頗為便利,下面我們來看幾個典型的範例。

範例1:簡單的開始

Test1()用以另一Thread執行Thread.Sleep()及Console.WriteLine(),效果與ThreadPool.QueueUserWorkItem()相當。

  private static void Test1()
        {
            //Task可以代替TheadPool.QueueUserWorkItem使用
            Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(2000);
                    Console.WriteLine("Done!");
                });
            Console.WriteLine("Async Run...");
        }

StartNew()完會立刻執行下一行,故會先看到Aync Run,1秒後列印出Done。

 

範例2:等待各作業完成再繼續下一步的應用場境

 

  同時啟動多個作業多工並行(多執行緒並行),但要等待各作業完成再繼續下一步的應用場境傳統方式上可通過WaitHandle、AutoResetEvent、ManualResetEvent等機制實現;Task的寫法相當簡單,建立多個Task物件,再作為Task.WaitAny()或Task.WaitAll()的引數就搞定了! 

private 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, task1);
            Console.WriteLine("WaitAny Passed");
            //等待所有作業完成後繼續
            Task.WaitAll(task1, task2);
            Console.WriteLine("WaitAll Passed");
        }

 

task1耗時3秒、task2耗時5秒,所以3秒後WaitAny()執行完成、5秒後WaitAll()執行完畢。

範例3:如果要等待多工作業傳回結果

通過StartNew<T>()指定傳回型別建立作業,隨後以Task.Result取值,不用額外Code就能確保多工作業執行完成後才讀取結果繼續執行

private 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執行並回傳結果的時間。

範例4:在多工作業完成後接連執行行另一段程式可使用ContinueWith():

   private 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完成後才被執行。

範例5:多工作業時各段邏輯便會依順序執行

.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秒後執行。

範例6:Task有監控狀態的機制

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等標識,並輸出結果。

  private static void Test6()
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken cancelToken = cts.Token;//獲取與此CancellationTokenSource關聯的CancellationToken
            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: //選3時
                        throw new ApplicationException("MyFaultException");
                }
                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.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();
            }
        }

Task能做的事,過去使用Thread/ThreadPool配合Event、WaitHandle一樣能辦到,但使用Task能以比較簡潔的語法完成相同工作,使用.NET 4.0開發多執行緒時可多加利用。

到這裡,我們繼續回到原本的.NET4.5中,首先我們設計幾種非同步作業新舊寫法法進行對比

利用WebClient類別下載網頁內容

1.使用Async/Complete設計模式

 private static void DownLoadWebPageSourceCode_Old()
        {
            WebClient wc = new WebClient();
            wc.DownloadStringCompleted += CompletedHandler;
            wc.DownloadStringAsync(new Uri("http://www.cnblogs.com/rohelm"));
            while (wc.IsBusy)
            {
                Console.WriteLine("還沒下完,我喝一回茶!");
            }
        }

        private static void CompletedHandler(object sender, DownloadStringCompletedEventArgs e)
        {
            Console.WriteLine(e.Result);
        }

執行效果如下:

2.使用新的async/await設計模式

 private static async void DownLoadWebPageSourceCode_New()
        {
            WebClient wc = new WebClient();
            Console.WriteLine(await wc.DownloadStringTaskAsync("http://www.cnblogs.com/rohelm"));
        }

而它的內部實現機制實際上是我們前面的附加專題中提到的Task,我們來檢視下這個方法的原始碼:

[ComVisible(false), HostProtection(SecurityAction.LinkDemand, ExternalThreading=true)]
public Task<string> DownloadStringTaskAsync(string address)
{
    return this.DownloadStringTaskAsync(this.GetUri(address));
}
 
[ComVisible(false), HostProtection(SecurityAction.LinkDemand, ExternalThreading=true)]
public Task<string> DownloadStringTaskAsync(Uri address)
{
    TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(address);
    DownloadStringCompletedEventHandler handler = null;
    handler = delegate (object sender, DownloadStringCompletedEventArgs e) {
        this.HandleCompletion<DownloadStringCompletedEventArgs, DownloadStringCompletedEventHandler, string>(tcs, e, args => args.Result, handler, delegate (WebClient webClient, DownloadStringCompletedEventHandler completion) {
            webClient.DownloadStringCompleted -= completion;
        });
    };
    this.DownloadStringCompleted += handler;
    try
    {
        this.DownloadStringAsync(address, tcs);
    }
    catch
    {
        this.DownloadStringCompleted -= handler;
        throw;
    }
    return tcs.Task;
}

3.取消非同步的方式

  由於上面我們已經說過它的內部本質還是Task所以它的,取消該非同步作業依舊藉助CancellationTokenSource及其所屬CancellationToken型別

private static async Task TryTask()
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            cts.CancelAfter(TimeSpan.FromSeconds(1));
            Task<string> task = Task.Run(() => PirntWords("Hello,Wrold!", cts.Token), cts.Token);
            Console.WriteLine(task.Result);
            await task;
        }

        private static string PirntWords(string input, CancellationToken token)
        {
            for (int i = 0; i < 20000000; i++)
            {
                Console.WriteLine(input);
                token.ThrowIfCancellationRequested();
            }
            return input;
        }

網頁呼叫用多個Web服務/WCF服務/Http服務模型

       public async Task<ActionResult> DoAsync()
        {
            ServiceClient1 client1 = new ServiceClient1();
            ServiceClient2 client2 = new ServiceClient2();
            var task1 = client1.GetDataAsync();
            var task2 = client2.GetDataAsync();
            await Task.WhenAll(task1,task2);
            return View("View名稱",new DataModel(task1.Result,task2.Rusult));
        }

 

是不是發現非常的方便,實用啊!

未完待續....

  後續內容:

   對WebAPI和WCF的進行一個簡單比較,探討WebAPI的機制,功能,架構,WinFrom Client/WebService Client大資料上傳...備註:本文章版權的沒有,歸.NET使用者共有。

相關文章