C#的變遷史 - C# 5.0 之並行編程總結篇

C# 5.0 搭載於.NET 4.5和VS2012之上。html

  同步操做既簡單又方便,咱們平時都用它。可是對於某些狀況,使用同步代碼會嚴重影響程序的可響應性,一般來講就是影響程序性能。這些狀況下,咱們一般是採用異步編程來完成功能,這在前面也屢次說起了。異步編程的核心原理也就是使用多線程/線程池和委託來完成任務的異步執行和返回,只不過在每一個新的C#版本中,微軟都替咱們完成了更多的事,使得程序模板愈來愈傻瓜化了。程序員


  .NET Framework 提供如下兩種執行 I/O 綁定和計算綁定異步操做的標準模式:
1. 異步編程模型 (APM,Asynchronous Programming Model)編程

  在該模型中異步操做由一對 Begin/End 方法(如 FileStream.BeginRead 和 Stream.EndRead)表示。
  異步編程模型是一種模式,該模式使用更少的線程去作更多的事。.NET Framework不少類實現了該模式,這些類都定義了BeginXXX和EndXXX相似的方法,好比FileStream類的BeginRead和EndRead方法。同時咱們也能夠自定義類來實現該模式(也就是在自定義的類中實現返回類型爲IAsyncResult接口的BeginXXX方法和EndXXX方法);另外委託類型也定義了BeginInvoke和EndInvoke方法,使得委託能夠異步執行。這些異步操做的背後都是線程池在支撐着,這是微軟異步編程的基礎架構,也是比較老的模式,不過從中咱們能夠清楚的瞭解異步操做的原理。多線程

  全部BeginXXX方法返回的都是實現了IAsyncResult接口的一個對象,並非對應的同步方法所要獲得的結果的。此時咱們須要調用對應的EndXXX方法來結束異步操做,並向該方法傳遞IAsyncResult對象,EndXxx方法的返回類型就是和同步方法同樣的。例如,FileStream的EndRead方法返回一個Int32來表明從文件流中實際讀取的字節數。架構

  對於訪問異步操做的結果,APM提供了四種方式供開發人員選擇:異步

- 在調用BeginXxx方法的線程上調用EndXXX方法來獲得異步操做的結果,可是這種方式會阻塞調用線程,直到操做完成以後調用線程才繼續運行
- 查詢IAsyncResult的AsyncWaitHandle屬性,從而獲得WaitHandle,而後再調用它的WaitOne方法來使一個線程阻塞並等待操做完成再調用EndXxx方法來得到操做的結果。
- 循環查詢IAsyncResult的IsComplete屬性,操做完成後再調用EndXxx方法來得到操做返回的結果。
- 使用 AsyncCallback委託來指定操做完成時要調用的方法,在操做完成後調用的方法中調用EndXxx操做來得到異步操做的結果。
  在上面的4種方式中,第4種方式是APM的首選方式,由於此時不會阻塞執行BeginXxx方法的線程,然而其餘三種都會阻塞調用線程,至關於效果和使用同步方法是同樣,在實際異步編程中都是使用委託的方式。async

看一個簡答的例子:異步編程

using System;
using System.Net;
using System.Threading;

class Program
{
    static DateTime start;
    static void Main(string[] args)
    {
        // 用百度分別檢索0,1,2,3,4,共檢索5次
        start = DateTime.Now;
        string strReq = "http://www.baidu.com/s?wd={0}";
        for (int i = 0; i < 5; i++)
        {
            var req = WebRequest.Create(string.Format(strReq, i));
            // 注意這裏的BeginGetResponse就是異步方法
            var res = req.BeginGetResponse(ProcessWebResponse, req);
        }

        Thread.Sleep(1000000);
    }

    private static void ProcessWebResponse(IAsyncResult result)
    {
        var req = (WebRequest)result.AsyncState;
        string strReq = req.RequestUri.AbsoluteUri;
        using (var res = req.EndGetResponse(result))
        {
            Console.Write("檢索 {0} 的結果已經返回!\t", strReq.Substring(strReq.Length - 1));
            Console.WriteLine("耗用時間:{0}毫秒", TimeSpan.FromTicks(DateTime.Now.Ticks - start.Ticks).TotalMilliseconds);
        }
    }
}

結構至關簡單,使用了回調函數獲取結果,就很少說了。函數

 

2. 基於事件的異步模式 (EAP,Event based Asynchronous programming Model)性能

  在該模式中異步操做由名爲「XXXAsync」和「XXXCompleted」的方法/事件表示,例如WebClient.DownloadStringAsync 和 WebClient.DownloadStringCompleted,還有像經常使用的BackgroundWorker.RunWorkerAsync和BackgroundWorker.RunWorkerCompleted方法。
  EAP 是在 .NET Framework 2.0 版中引入的。使用陳舊的BeginXXX和EndXXX方法無疑是不夠優雅的,而且程序員須要寫更多的代碼,特別是在UI程序中使用不太方便。UI的各類操做基本都是基於事件的,並且一般來講UI線程和子線程之間還須要互相交流,好比說顯示進度,警告,相關的消息等等,直接在子線程中訪問UI線程上的空間是須要寫一些同步代碼的。這些操做使用APM處理起來都比較麻煩,而EAP則很好的解決了這些問題,EAP裏面最出色的表明就應該是BackgroundWorker類了。
  看一個網上一位仁兄寫的下載的小例子:

private void btnDownload_Click(object sender, EventArgs e)
{
    if (bgWorkerFileDownload.IsBusy != true)
    {
       // 開始異步執行DoWork中指定的任務 
       bgWorkerFileDownload.RunWorkerAsync();
                  
       // 建立RequestState對象
       requestState = new RequestState(downloadPath);
       requestState.filestream.Seek(DownloadSize, SeekOrigin.Begin);
       this.btnDownload.Enabled = false;
       this.btnPause.Enabled = true;
    }
    else
    {
       MessageBox.Show("正在執行操做,請稍後");
    }
}

private void btnPause_Click(object sender, EventArgs e)
{
  // 暫停的標準處理方式:先判斷標識,而後異步申請暫停
  if (bgWorkerFileDownload.IsBusy && bgWorkerFileDownload.WorkerSupportsCancellation == true)
  {
    bgWorkerFileDownload.CancelAsync();
  }
}

// 指定Worker的工做任務,當RunWorkerAsync方法被調用時開始工做
// 這是在子線程中執行的,不容許訪問UI上的元素
private void bgWorkerFileDownload_DoWork(object sender, DoWorkEventArgs e)
{
    // 獲取事件源
    BackgroundWorker bgworker = sender as BackgroundWorker;

    // 開始下載
    HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim());

    // 斷點續傳的功能
    if (DownloadSize != 0)
    {
      myHttpWebRequest.AddRange(DownloadSize);
    }

    requestState.request = myHttpWebRequest;
    requestState.response = (HttpWebResponse)myHttpWebRequest.GetResponse();
    requestState.streamResponse = requestState.response.GetResponseStream();
    int readSize = 0;
    // 前面講過的異步取消中子線程的工做:循環並判斷標識
    while (true)
    {
      if (bgworker.CancellationPending == true)
      {
        e.Cancel = true;
        break;
      }

      readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length);
      if (readSize > 0)
      {
        DownloadSize += readSize;
        int percentComplete = (int)((float)DownloadSize / (float)totalSize * 100);
        requestState.filestream.Write(requestState.BufferRead, 0, readSize);

        // 報告進度,引起ProgressChanged事件的發生
        bgworker.ReportProgress(percentComplete);
      }
      else
      {
        break;
      }
    }
}

// 當Worker執行ReportProgress時回調此函數。此函數在UI線程中執行更新操做進度的任務
// 由於是在在主線程中工做的,能夠與UI上的元素交互
private void bgWorkerFileDownload_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    this.progressBar1.Value = e.ProgressPercentage;
}

// 當Worker結束時觸發的回調函數:也許是成功完成的,或是取消了,或者是拋異常了。
// 這個方法是在UI線程中執行,因此能夠與UI上的元素交互
private void bgWorkerFileDownload_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Error != null)
    {
      MessageBox.Show(e.Error.Message);
      requestState.response.Close();
    }
    else if (e.Cancelled)
    {
      MessageBox.Show(String.Format("下載暫停,下載的文件地址爲:{0}\n 已經下載的字節數爲: {1}字節", downloadPath, DownloadSize));
      requestState.response.Close();
      requestState.filestream.Close();

      this.btnDownload.Enabled = true;
      this.btnPause.Enabled = false;
    }
    else
    {
      MessageBox.Show(String.Format("下載已完成,下載的文件地址爲:{0},文件的總字節數爲: {1}字節", downloadPath, totalSize));

      this.btnDownload.Enabled = false;
      this.btnPause.Enabled = false;
      requestState.response.Close();
      requestState.filestream.Close();
    }
}

private void GetTotalSize()
{
    HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim());
    HttpWebResponse response = (HttpWebResponse)myHttpWebRequest.GetResponse();
    totalSize = response.ContentLength;
    response.Close();
}

// 存儲申請的狀態
public class RequestState
{
    public int BufferSize = 2048;

    public byte[] BufferRead;
    public HttpWebRequest request;
    public HttpWebResponse response;
    public Stream streamResponse;

    public FileStream filestream;
    public RequestState(string downloadPath)
    {
      BufferRead = new byte[BufferSize];
      request = null;
      streamResponse = null;
      filestream = new FileStream(downloadPath, FileMode.OpenOrCreate);
    }
}

  上面的例子就是實現了一個能夠取消的帶斷點續傳功能的下載器,這是個Winform程序,控件也很簡單:一個Label,一個Textbox,兩個Button,一個ProgressBar;把這些控件和上面的事件對應綁定便可。  

 

  在.NET 4.0 (C# 4.0)中,並行庫(TPL)的加入使得異步編程更加方便快捷,在.NET 4.5 (C# 5.0)中,異步編程將更加方便。

  這裏咱們先回顧一下C# 4.0中的TPL的用法,看一個簡單的小例子:這個例子中只有一個Button和一個Label,點擊Button會調用一個函數計算一個結果,這個結果最後會顯示到Label上,很簡單,咱們只看核心的代碼:

private void button1_Click(object sender, EventArgs e)
{
    this.button1.Enabled = false;
    var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); //get UI thread context 
    var someTask = Task<int>.Factory.StartNew(() => slowFunc(1, 2)); //create and start the Task 
    someTask.ContinueWith(x =>
        {
            this.label1.Text = "Result: " + someTask.Result.ToString();
            this.button1.Enabled = true;
        }, uiScheduler
    );
}

private int slowFunc(int a, int b)
{
    System.Threading.Thread.Sleep(3000);
    return a + b;
}

  上面的slowFunc就是模擬了一個須要大量時間去運行的任務,爲了避免阻塞UI線程,只能使用Task去異步運行,爲了在把結果顯示到Label上,代碼中咱們使用了TaskScheduler.FromCurrentSynchronizationContext()方法同步線程上下文,使得在ContinueWith方法中可使用UI線程上的控件,這是TPL編程中的一個經常使用技巧。
  說不上太麻煩,可是感受上總之不舒服,徹底沒有同步代碼寫起來那麼天然,簡單。從我我的的理解來講,C# 5.0中的async和await正是提升了這方面的用戶體驗。
  C# 5.0中的async和await特性並無在IL層面增長了新的成員,因此也能夠說是一種語法糖。下面先看看再C# 5.0中如何解決這個問題: 

private async void button1_Click(object sender, EventArgs e)
{
    this.button1.Enabled = false;
    var someTask = Task<int>.Factory.StartNew(() => slowFunc(1, 2));
    await someTask;
    this.label1.Text = "Result: " + someTask.Result.ToString();
    this.button1.Enabled = true;
}

  注意這段代碼中的async和await的用法。除了這個事件處理函數,其餘的都沒有變化。是否是很神奇,徹底和同步代碼沒什麼太大的區別,非常簡單優雅,徹底是同步方式的異步編程
  下面咱們就詳細的討論一下async和await這兩個關鍵字。

async和await
  經過使用async修飾符,可將方法、lambda表達式或匿名方法指定爲異步。 使用了這個修飾符的方法或表達式,則其稱爲異步方法,如上面的button1_Click方法就是一個異步方法。
  異步方法提供了一種簡便方式來完成可能須要長時間運行的工做,而沒必要阻塞調用方的線程。 異步方法的調用方(這裏就是button1_Click的調用者)能夠繼續工做,而沒必要等待異步方法button1_Click完成。 完成這個特性須要使用 await 關鍵字,以便當即返回,從而容許button1_Click的調用方繼續工做或返回到線程的同步上下文(或消息泵)。
  從上面的描述中獲得,異步方法更準確的定義應該是:使用async修飾符定義的,且一般包含一個或多個await表達式的方法稱爲異步方法
  若是async關鍵字修飾的方法不包含await表達式或語句,則該方法仍將同步執行。 對於這種狀況,編譯器將會給出警告,由於該狀況一般表示程序可能存在錯誤。 也就是說,單單使用async修飾符的方法仍是在同步執行的,只有配合await關鍵字後方法的部分纔開始異步執行。

  await表達式不阻塞主線程。 相反,它告訴編譯器去重寫異步方法來完成下面幾件事:
1. 啓動子線程(一般是線程池中的線程)完成await表達式中指定的任務,這是異步執行的真正含義。
2. 將await表達式後面未執行的語句註冊爲await表達式中執行的任務的後續任務,而後掛起這個異步方法,直接返回到異步方法的調用方。
3. 當await表達式中執行的任務完成後,子線程結束。
4. 任務尋找到註冊的後續任務,恢復異步方法的執行環境,繼續執行後續任務,由於已經恢復到異步方法的執行上下文中,因此不存在跨線程的問題。
  看了這個過程,其實與咱們使用ContinueWith的那種方式沒什麼太大的不一樣。回到上面的button1_Click方法,這下就好理解了,該方法從開始時同步運行,直至到達其第一個await表達式,此時異步的執行Task中指定的方法,而後將button1_Click方法掛起,回到button1_Click的調用者執行其餘的代碼;直到等待的任務完成後,回到button1_Click中繼續執行後續的代碼,也就是更新Label的內容。

  這裏須要注意幾點:
1. async和await只是上下文關鍵字。 當它們不修飾方法、lambda 表達式或匿名方法時,就不是關鍵字了,只做爲普通的標識符。
2. 使用async修飾的異步方法的返回類型能夠爲 Task、Task<TResult> 或 void。 方法不能聲明任何 ref 或 out 參數,可是能夠調用具備這類參數的方法。
  若是異步方法須要一個 TResult 類型的返回值,則須要應指定 Task<TResult> 做爲方法的返回類型。
  若是當方法完成時未返回有意義的值,則應使用 Task。 對於返回Task的異步方法,當 Task 完成時,任何等待 Task 的全部 await 表達式的計算結果都爲 void。
  而使用void做爲返回類型的方式主要是來定義事件處理程序,這些處理程序須要此返回類型。 使用void 做爲異步方法的返回值時,該異步方法的調用方不能等待,而且沒法捕獲該方法引起的異常。
3. await表達式的返回值
  若是 await 應用於返回Task<TResult>的方法調用的結果,那麼 await 表達式的類型是 TResult。 若是將 await 應用於返回Task的方法調用結果,則 await 表達式的類型無效。看下面的例子中的使用方式:

// 返回Task<TResult>的方法.
TResult result = await AsyncMethodThatReturnsTaskTResult();

// 返回一個Task的方法.
await AsyncMethodThatReturnsTask();

4.異常問題
  大多數異步方法返回 Task 或 Task<TResult>。 返回任務的屬性承載有關其狀態和歷史記錄的信息,例如任務是否已完成,異步方法是否引起異常或已取消,以及最終結果如何。 await 運算符會訪問那些屬性。
  若是任務返回異常,await 運算符會再次引起異常。
  若是任務被取消後返回,await 運算符也會再次引起 OperationCanceledException。
  總之,在await外圍使用try/catch能夠捕獲任務中的異常。看一個例子:

public class AsyncTest
{
    static void Main(string[] args)
    {
        AsyncTest c = new AsyncTest();
        c.RunAsync();

        // 模擬其餘的工做
        Thread.Sleep(1000000);
    }

    public void RunAsync()
    {
        DisplayValue(); 
        //這裏不會阻塞
        Console.WriteLine("RunAsync() End.");
    }

    public Task<double> GetValueAsync(double num1, double num2)
    {
        return Task.Run(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                num1 = num1 / num2;

                if (i == 999999)
                {
                    throw new Exception("Crash");
                }
            }

            return num1;
        });
    }

    public async void DisplayValue()
    {
        double result = 0;
        //此處會開新線程處理GetValueAsync任務,而後方法立刻返回
        try
        {
            result = await GetValueAsync(1234.5, 1.0);
        }
        catch (Exception)
        {
            //throw;
        }
            
        //這以後的全部代碼都會被封裝成委託,在GetValueAsync任務完成時調用
        Console.WriteLine("Value is : " + result);
    }
}

  可是須要注意一點,若是任務拋出了多個異常(例如,該任務多是啓動了更多的子線程)時,await運算符只能拋出異常中的一個,並且不能肯定是哪個。這時就須要把這些子線程包裝到一個Task中,這樣這些異常就都會被包裝到AggregateException中,看下面例子的作法:

public class AsyncTest
{
    static void Main(string[] args)
    {
        AsyncTest c = new AsyncTest();
        c.RunAsync();

        // 模擬其餘的工做
        Thread.Sleep(1000000);
    }

    public void RunAsync()
    {
        DisplayValue(); 
        //這裏不會阻塞
        Console.WriteLine("RunAsync() End.");
    }

    public async void DisplayValue()
    {
        Task all = null;
        try
        {
            await (all = Task.WhenAll(
                Task.Run(() => { throw new Exception("Ex1"); }), 
                Task.Run(() => { throw new Exception("Ex2"); }))
                );
        }
        catch
        {
            foreach (var ex in all.Exception.InnerExceptions)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

  固然了,你們也別忘了最後一招殺手鐗:TaskScheduler.UnobservedTaskException,使用這個去捕獲一些沒有處理的異常。
  到此,異步方法就介紹到這裏了。最後附上一位網上兄弟寫的異步執行一些耗時操做的輔助類:

public static class TaskAsyncHelper
{
    /// <summary>
    /// 將一個方法function異步運行,在執行完畢時執行回調callback
    /// </summary>
    /// <param name="function">異步方法,該方法沒有參數,返回類型必須是void</param>
    /// <param name="callback">異步方法執行完畢時執行的回調方法,該方法沒有參數,返回類型必須是void</param>
    public static async void RunAsync(Action function, Action callback)
    {
        Func<System.Threading.Tasks.Task> taskFunc = () =>
        {
            return System.Threading.Tasks.Task.Run(() =>
            {
                function();
            });
        };
        await taskFunc();
        if (callback != null)
            callback();
    }

    /// <summary>
    /// 將一個方法function異步運行,在執行完畢時執行回調callback
    /// </summary>
    /// <typeparam name="TResult">異步方法的返回類型</typeparam>
    /// <param name="function">異步方法,該方法沒有參數,返回類型必須是TResult</param>
    /// <param name="callback">異步方法執行完畢時執行的回調方法,該方法參數爲TResult,返回類型必須是void</param>
    public static async void RunAsync<TResult>(Func<TResult> function, Action<TResult> callback)
    {
        Func<System.Threading.Tasks.Task<TResult>> taskFunc = () =>
        {
            return System.Threading.Tasks.Task.Run(() =>
            {
                return function();
            });
        };
        TResult rlt = await taskFunc();
        if (callback != null)
            callback(rlt);
    }
}

簡單實用!

 

推薦連接:你必須知道的異步編程:http://www.cnblogs.com/zhili/category/475336.html傳統異步編程指導:http://msdn.microsoft.com/zh-cn/library/vstudio/dd997423.aspx使用async異步編程指導:http://msdn.microsoft.com/zh-cn/library/vstudio/hh191443.aspx

相關文章
相關標籤/搜索