C#圖解教程 第二十章 異步編程

筆記


啓動一個程序,系統在內存中建立一個新進程,進程內部是系統建立的線程,線程能夠派生其餘線程,這就有了多線程。
進程內的多個線程共享進程的資源,系統爲處理器規劃的單元是線程。程序員

異步編程能夠實如今新線程裏面運行一部分代碼,或改變代碼的執行順序。編程

本章介紹瞭如下幾種異步編程方式,它們居可能是併發的而非並行。數組

  • async/await
    • .NET4.5以上才支持(4.0能夠用擴展包支持)
    • 簡單易用,結合異步方法的控制流,結構清晰明瞭
    • 三種返回模式
    • 能夠在調用方法中同步/異步地等待任務
    • 支持異步Lambda表達式以執行簡單程序
    • 適合那些須要在後臺完成的不相關的小任務
  • BackgroundWorker
    • .NET支持較早
    • 在後臺持續運行,並不時與主線程通訊
    • 在WinForm中比較適合在須要與UI層通訊時使用
    • HelloAsync項目裏面的FormBackgroundWorker就是它的示例
  • Task Parellel
    • System.Threading.Tasks中的Parallel.ForParallel.ForEach
    • 這是真正的多核處理器並行執行程序
  • BeginInvoke/EndInvoke
  • System.Threading.Timer

異步編程

什麼是異步


啓動程序時,系統會在內存中建立一個新的進程。進程是構成運行程序的資源集合。包括虛地址空間、文件句柄和許多其餘程序運行所需的東西。服務器

在進程內部,系統建立了一個稱爲線程的內核(kernel)對象,它表明了真正執行的程序。(線程是「執行線程」的簡稱)一旦進程創建,系統會在Main方法的第一行語句處就開始線程的執行。網絡

關於線程,須要瞭解如下知識點多線程

  • 默認狀況下,一個進程只包含一個線程,從程序的開始一直執行到結束
  • 線程能夠派生其餘線程,所以在任意時刻,一個進程均可能包含不一樣狀態的多個線程,來執行程序的不一樣部分
  • 若是一個進程擁有多個線程,它們將共享進程的資源
  • 系統爲處理器執行所規劃的單元是線程,不是進程

本書目前爲止所展現的全部示例程序都只使用了一個線程,而且從程序的第一條語句按順序執行到最後一條。然而在不少狀況下,這種簡單的模型都會在性能或用戶體驗上致使難以接受的行爲。架構

例如,一個服務器程序可能會持續不斷地發起到其餘服務器的鏈接,並向它們請求數據,同時處理來自多個客戶端程序的請求。這種通訊任務每每耗費大量時間,在此期間程序只能等待網絡或互聯網上其餘計算機的響應。這嚴重削弱了性能。程序不該該浪費等待響應的時間,而應該更加高效,在等待的同時執行其餘任務,回覆到達後再繼續執行第一個任務。併發

本章咱們將學習異步編程。在異步程序中,程序代碼不須要按照編寫的順序嚴格執行。有時須要在一個新的線程中運行一部分代碼,有時無需建立新的線程,但爲了更好地利用單個線程的能力,須要改變代碼的執行順序。框架

咱們先來看看C#5.0引入的一個用來構建異步方法的新特性——async/await。接下來學習一些可實現其餘形式的異步編程的特性,這些特性是.NET框架的一部分,但沒有嵌入C#語言。相關主題包括BackgroundWorker類和.NET任務並行庫。二者均經過新建線程來實現異步。本章最後咱們會看看編寫異步程序的其餘方式。異步

示例

爲了演示和比較,咱們先來看一個不使用異步的示例。而後再看一個實現相似功能的異步程序。

在下面的代碼示例中,MyDownloadString類的方法DoRun執行如下任務。

  • 建立Stopwatch類(位於System.Diagnostics命名空間)的一個實例並啓動。該Stopwatch計時器用來測量代碼中不一樣任務的執行時間
  • 而後兩次調用CountCharacters方法,下載某網站的內容,並返問該網站包含的字符數。網站由URL字符串指定,做爲第二個參數傳入
  • 接着四次調用CountToALargeNumber方法。該方法僅執行一個消耗必定時間的任務,並循環指定次數
  • 最後,打印兩個網站的字符數
using System;
using System.Net;
using System.Diagnostics;
class MyDownloadString
{
    Stopwatch sw = new Stopwatch();
    public void DoRun()
    {
        const int LargeNumber = 6000000;
        sw.Start();
        int t1 = CountCharacters(1, "http://www.microsoft.com");
        int t2 = CountCharacters(2, "http://www.illustratedcsharp.com");
        CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber);
        CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber);
        Console.WriteLine("Chars in http://www.microsoft.coin  :{0}", t1);
        Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2);
    }
    private int CountCharacters(int id, string uriString)
    {
        WebClient wc1 = new WebClient();
        Console.WriteLine("Starting call {0}     :    {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds);
        string result = wc1.DownloadString(new Uri(uriString));
        Console.WriteLine("    Call {0} completed:    {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds);
        return result.Length;
    }
    private void CountToALargeNumber(int id, int value)
    {
        for (long i = 0; i < value; i++)
            ;
        Console.WriteLine("    End counting {0}  :    {1,4:N} ms", id, sw.Elapsed.TotalMilliseconds);
    }
}
class Program
{
    static void Main()
    {
        MyDownloadString ds = new MyDownloadString();
        ds.DoRun();
        Console.ReadKey();
    }
}

輸出:

Starting call 1     :    0.39 ms
    Call 1 completed:    131.95 ms
Starting call 2     :    132.04 ms
    Call 2 completed:    655.97 ms
    End counting 1  :    670.80 ms
    End counting 2  :    685.57 ms
    End counting 3  :    700.00 ms
    End counting 4  :    714.46 ms
Chars in http://www.microsoft.coin  :1020
Chars in http://www.illustratedcsharp.com: 210

下圖總結了輸出結果,展現了不一樣任務開始和結束的時間。如圖所示,Call1和Call2佔用了大部分時間。但無論哪次調用,絕大部分時間都浪費在等待網站的響應上。

若是咱們能初始化兩個CountCharacter調用,無需等待結果,而是直接執行4個CountToALargeNumber調用,而後在兩個CountCharacter方法調用結束時再獲取結果,就能夠顯著地提高性能。

C#最新的async/await特性就容許咱們這麼作。能夠重寫代碼以運用該特性,以下所示。稍後我會深刻剖析這個特性,如今先來看看本示例須要注意的幾個方面。

  • 當DoRun調用CountCharactersAsync時,CountCharactersAsync將當即返回,而後才真正開始下載字符。它向調用方法返回的是一個Task<int>類型的佔位符對象,表示它計劃進行的工做。這個佔位符最終將「返回」一個int
  • 這使得DoRun不用等待實際工做完成就可繼續執行。下一條語句是再次調用 CountCharactersAsync,一樣會返回一個Task<int>對象
  • 接着,DoRun能夠繼續執行,調用4次 CountToALargeNumber,同時 CountCharactersAsync 的兩次調用繼續它們的工做——基本上是等待(網站的響應)
  • DoRun 的最後兩行從 CountCharactersAsync 調用返回的 Tasks 中獲取結果。若是尚未結果,將阻塞並等待
using System;
using System.Diagnostics;
using System.Net;
using System.Threading.Tasks;
class MyDownloadString
{
    Stopwatch sw = new Stopwatch();
    public void DoRun()
    {
        const int LargeNumber = 6000000;
        sw.Start();
        Task<int> t1 = CountCharactersAsync(1, "http://www.microsoft.com");
        Task<int> t2 = CountCharactersAsync(2, "http://www.illustratedcsharp.com");
        CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber);
        CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber);
        Console.WriteLine("Chars in http://www.microsoft.coin  :{0}", t1.Result);
        Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2.Result);
    }
    private async Task<int> CountCharactersAsync(int id, string site)
    {
        WebClient wc = new WebClient();
        Console.WriteLine("Starting call {0}     :    {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds);
        string result = await wc.DownloadStringTaskAsync(new Uri(site));
        Console.WriteLine(" Call {0} completed   :    {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds);
        return result.Length;
    }
    private void CountToALargeNumber(int id, int value)
    {
        for (long i = 0; i < value; i++)
            ;
        Console.WriteLine("    End counting {0}  :    {1,4:N} ms", id, sw.Elapsed.TotalMilliseconds);
    }
}
class Program
{
    static void Main()
    {
        MyDownloadString ds = new MyDownloadString();
        ds.DoRun();
        Console.ReadKey();
    }
}

輸出:

Starting call 1     :    1.33 ms
Starting call 2     :    66.50 ms
    End counting 1  :    83.81 ms
    End counting 2  :    124.33 ms
 Call 1 completed   :    124.35 ms
    End counting 3  :    138.55 ms
    End counting 4  :    152.52 ms
Chars in http://www.microsoft.coin  :1020
 Call 2 completed   :    623.79 ms
Chars in http://www.illustratedcsharp.com: 210

下圖總結了輸出結果,展現了修改後的程序的時間軸。新版程序比舊版快了32%。這是因爲 CountToALargeNumber 的4次調用是在 CountCharactersAsync 方法調用等待網站響應的時候進行的。全部這些工做都是在主線程中完成的,咱們沒有建立任何額外的線程!

async/await特性的結構


咱們已經看到了一個異步方法的示例,如今來討論其定義和細節。
若是一個程序調用某個方法,等待其執行全部處理後才繼續執行,咱們就稱這樣的方法是同步的。這是默認形式,在本章以前你所看到的都是這種形式。
相反,異步的方法在處理完成以前就返回到調用方法。C#的async/await特性能夠建立並使用異步方法。該特性由三個部分組成,以下所示。

  • 調用方法(calling method):該方法調用異步方法,而後在異步方法(可能在相同的線程,也可能在不一樣的線程)執行其任務的時候繼續執行
  • 異步(async)方法:該方法異步執行其工做,而後當即返回到調用方法
  • await表達式:用於異步方法內部,指明須要異步執行的任務。一個異步方法能夠包含任意多個await表達式,不過若是一個都不包含的話編譯器會發出警告
Class Program
{
    static void Main()
    {
        ...
        //調用方法
        Task<int> value=DoAsyncStuff.CalculateSumAsync(5,6);
        ...
    }
}
static class DoAsyncStuff
{
    //異步方法
    public static async Task<int> CalculateSumAsync(int i1,int i2)
    {
        //await表達式
        int sum=await TaskEx.Run(()=>GetSum(i1,i2));
        return sum;
    }
    ...
}

 

什麼是異步方法


如上節所述,異步方法在完成其工做以前即返回到調用方法,而後在調用方法繼續執行的時候完成其工做。
在語法上,異步方法具備以下特色,以下圖。

  • 方法頭中包含async方法修飾符
  • 包含一個或多個await表達式,表示能夠異步完成的任務。
  • 必須具有如下三種返回類型。第二種(Task)和第三種(Task<T>)的返回對象表示將在將來完成的工做,調用方法和異步方法能夠繼續執行
    • void
    • Task
    • Task<T>
  • 異步方法的參數能夠爲任意類型任意數量,但不能爲out或ref參數
  • 按照約定,異步方法的名稱應該以Async爲後綴
  • 除了方法之外,Lambda表達式和匿名方法也能夠做爲異步對象。
關鍵字  返回類型
  ↓        ↓
async Task<int> CountCharactersAsync(int id,string site)
{
    WebClient wc = new WebClient();
    Console.WriteLine( "Starting call {0}   :   {1, 4:N} ms",id, sw.Elapsed.TotalMilliseconds);
    // await表達式
    string result = await wc.DownloadStringTaskAsync( new Uri(site));
    Console.WriteLine( " Call {0} completed:    {1, 4:N} ms",id, sw.Elapsed.TotalMilliseconds);
    // 返回語句
    return result.Length;
}

上例闡明瞭一個異步方法的組成部分,如今咱們能夠詳細介紹了。
第一項是async關鍵字。

  • 異步方法在方法頭中必須包含async關鍵字,且必須出如今返回類型以前
  • 該修飾符只是標識該方法包含一個或多個await表達式。也就是說,它自己並不能建立任何異步操做。
  • async關鍵字是一個上下文關鍵字,也就是說除了做爲方法修飾符(或Lambda表達式修飾符、匿名方法修飾符)以外,async還可用做標識符

返回類型必須是如下三種類型之一。注意,其中兩種都涉及Task類。我在指明類的時候,將使用大寫形式(類名)和語法字體來區分。在表示一系列須要完成的工做時,將使用小寫字母和通常字體。

  • Task<T>:若是調用方法要從調用中獲取一個T類型的值,異步方法的返回類型就必須是Task<T>。調用方法將經過讀取Task的Result屬性來獲取這個T類型的值。下面的代碼來自一個調用方法,闡明瞭這一點:
Task<int> value = DoStuff.CalculateSumAsync(5,6);
...
Console,WriteLine( "Value: {0}", value.Result);
  • Task:若是調用方法不須要從異步方法中返回某個值,但須要檢査異步方法的狀態,那麼異步方法能夠返回一個Task類型的對象。這時,即便異步方法中出現了 return語句,也不會返回任何東西。下面的代碼一樣來自調用方法:
Task someTask = DoStuff.CalculateSumAsync(5,6);
...
someTask.Wait();
  • void:若是調用方法僅僅想執行異步方法,而不須要與它作任何進一步的交互時[這稱爲調用並忘記(fire and forget)],異步方法能夠返回void類型。這時,與上一種狀況相似,即便異步方法中包含任何return語句,也不會返回任何東西

注意上例中異步方法的返回類型爲Task<int>。但方法體中不包含任何返回Task<int>類型對象的return語句。相反,方法最後的return語句返回了一個int類型(result.Length)的值。咱們先將這一發現總結以下,稍後再詳細解釋。

  • 任何返回Task<T>類型的異步方法其返回值必須爲T類型或能夠隱式轉換爲T的類型

下面闡明瞭調用方法和異步方法在用這三種返回類型進行交互時所需的體系結構。
使用返回Task<int>對象的異步方法

using System;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        Task<int> value=DoAsyncStuff.CalculateSumAsync(5,6);
        //處理其餘事情
        Console.WriteLine("Value: {0}",value.Result);
    }
}
static class DoAsyncStuff
{
    public static async Task<int> CalculateSumAsync(int i1,int i2)
    {
        int sum=await Task.Run(()=>GetSum(i1,i2));
        return sum;
    }
    private static int GetSum(int i1,int i2)
    {
        return i1+i2;
    }
}

使用返回Task對象的異步方法

using System;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        Task someTask=DoAsyncStuff.CalculateSumAsync(5,6);
        //處理其餘事情
        someTask.Wait();
        Console.WriteLine("Async stuff is done");
    }
}
static class DoAsyncStuff
{
    public static async Task CalculateSumAsync(int i1,int i2)
    {
        int value=await Task.Run(()=>GetSum(i1,i2));
        Console.WriteLine("Value: {0}",value);
    }
    private static int GetSum(int i1,int i2)
    {
        return i1+i2;
    }
}

輸出:

Value: 11
Async stuff is done

下例中使用Thread.Sleep方法來暫停當前線程,因此異步方法完成時,它尚未完成。
使用「調用並忘記」的異步方法

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        DoAsyncStuff.CalculateSumAsync(5,6);
        //處理其餘事情
        Thread.Sleep(200);
        Console.WriteLine("Program Exiting");
    }
}
static class DoAsyncStuff
{
    public static async void CalculateSumAsync(int i1,int i2)
    {
        int value=await Task.Run(()=>GetSum(i1,i2));
        Console.WriteLine("Value: {0}",value);
    }
    private static int GetSum(int i1,int i2)
    {
        return i1+i2;
    }
}

輸出:

Value: 11
Program Exiting

異步方法的控制流

異步方法的結構包含三個不一樣的區域,以下圖所示。我將在下節詳細介紹await表達式,不過在本節你將對其位置和做用有個大體瞭解。這三個區域以下:

  • 第一個await表達式以前的部分:從方法開頭到第一個await表達式之間的全部代碼。這一部分應該只包含少許且無需長時間處理的代碼
  • await表達式:表示將被異步執行的任務
  • 後續部分:在await表達式以後出現的方法中的其他代碼。包括其執行環境,如所在線程信息、目前做用域內的變量值,以及當await表達式完成後要從新執行所需的其餘信息

下圖闡明瞭一個異步方法的控制流。它從第一個await表達式以前的代碼開始,正常執行 (同步地)直到碰見第一個await。這一區域實際上在第一個await表達式處結束,此時await任務尚未完成(大多數狀況下如此)。當await任務完成時,方法將繼續同步執行。若是還有其餘await,就重複上述過程。
當達到await表達式時,異步方法將控制返回到調用方法。若是方法的返回類型爲TaskTask<T>類型,將建立一個Task對象,表示需異步完成的任務和後續,而後將該Task返回到調用方法。

目前有兩個控制流:異步方法內的和調用方法內的。異步方法內的代碼完成如下工做。

  • 異步執行await表達式的空閒任務
  • 當await表達式完成時,執行後續部分。後續部分自己也可能包含其餘await表達式,這些表達式也將按照相同的方式處理,即異步執行await表達式,而後執行後續部分
  • 當後續部分遇到return語句或到達方法末尾時,將:
    • 若是方法返回類型爲void,控制流將退出
    • 若是方法返冋類型爲Task,後續部分設置Task的屬性並退出。若是返回類型爲Task<T>,後續部分還將設置Task對象的 Result 屬性

同時,調用方法中的代碼將繼續其進程,從異步方法獲取Task對象。當須要其實際值時,就引用Task對象的 Result 屬性。屆時,若是異步方法設置了該屬性,調用方法就能得到該值並繼續。不然,將暫停並等待該屬性被設置,而後再繼續執行。

不少人可能不解的一點是同步方法第一次遇到await時所返回對象的類型。這個返回類型就是同步方法頭中的返回類型,它與await表達式的返回值類型一點關係也沒有。
例以下面的代碼,await表達式返回一個string。但在方法的執行過程當中,當到達await表達式時,異步方法返回到調用方法的是一個Task<int>對象,這正是該方法的返回類型。

private async Task<int> CountCharactersAsync(string site)
{
    WebClient wc=new WebClient();
    string result=await wc.DownloadStringTaskAsync(new Uri(site));
    return result.Length;
}

另外一個可能讓人迷惑的地方是,異步方法的return語句「返回」一個結果或到達異步方法末尾時,它並無真正地返回某個值——它只是退出了。

await表達式

await表達式指定了一個異步執行的任務。其語法以下所示,由await關鍵字和一個空閒對象 (稱爲任務)組成。這個任務多是一個Task類型的對象,也可能不是。默認狀況下,這個任務在當前線程異步運行。
await task
一個空閒對象便是一個awaitable類型的實例。awaitable類型是指包含GetAwaiter方法的類型,該方法沒有參數,返回一個稱爲awaiter類型的對象。awaiter類型包含如下成員:

  • bool IsCompleted{get;}
  • void OnCompleted(Action);

它還包含如下成員之一:

  • void GetResult();
  • T GetResult();//T爲任意類型

然而實除上,你並不須要構建本身的awaitable。相反,你應該使用Task類,它是awaitable類型。對於awaitable,大多數程序員所須要的就是Task了。
在.NET4.5中,微軟發佈了大量新的和修訂的異步方法(在BCL中),它們可返回Task<T>類型的對象。將這些放到你的await表達式中,它們將在當前線程中異步執行。
在以前的不少示例中,咱們都使用了WebClient.DownloadStringTaskAsync方法,它也是這些異步方法中一個。如下代碼闡明瞭其用法:

Uri site = new Uri("http://www.illustratedcsharp.com");
WebClient wc = new WebClient();
string result = await wc.DownloadStringTaskAsync(site);

儘管目前BCL中存在不少返回Task<T>類型對象的方法,你仍然可能須要編寫本身的方法, 做爲await表達式的任務。最簡單的方式是在你的方法中使用Task.Run方法來建立一個Task。關於Task.Run,有一點很是重要,即它是在不一樣的線程上運行你的方法
Task.Run的一個簽名以下,以Func<TReturn>委託(Delegate)爲參數。如第19章所述,Func<TReturn>是一個預約義的委託,它不包含任何參數,返回值的類型爲TReturn:
Task Run(Func<TReturn> func)
所以,要將你的方法傳遞給Task.Run方法,須要基於該方法建立一個委託。下面的代碼展現了三種實現方式。其中,Get10與Func<int>委託兼容,由於它沒有參數而且返回int。

  • 第一個實例(DoWorkAsync方法的前兩行)使用Get10建立名爲ten的Func<int>委託。而後在下一行將該委託用於Task.Run方法
  • 第二個實例在了Task.Run方法的參數列表中建立Func<int>委託
  • 第三個實例沒有使用Get10方法。而是使用了組成Get10方法的return語句,將其用於與Func<int>委託兼容的Lambda表達式。該Lambda表達式將隱式轉換爲該委託
class MyClass
{
    public int Get10()
    {
        return 10;
    }
    public async Task DoWorkAsync()
    {
        // 單首創建 Func<TReturn> 委託
        Func<int> ten=new Func<int>(Get10);
        int a=await Task.Run(ten);
        // 參數列表中建立 Func<TReturn> 委託
        int b=await Task.Run(new Func<int>(Get10));
        // 隱式轉換爲 Func<TReturn> 委託的 Lambda表達式
        int c=await Task.Run(()=>{return 10;});
        Console.WriteLine("{0}  {1}  {2}",a,b,c);
    }
}
class Program
{
    static void Main()
    {
        Task t=(new MyClass()).DoWorkAsync();
        t.Wait();
    }
}

輸出:

10  10  10

在上面的示例代碼中,咱們使用的Task.Run的簽名以Func<TResult>爲參數。該方法共有8個重載,以下表所示。

下表展現了可能用到的4個委託類型的簽名。

下面的代碼展現了4個await語句,使用Task.Run方法來運行4種不一樣的委託類型所表示的方法:

static class MyClass
{
    public static async Task DoWorkAsync()
    {                               Action
                                      ↓
        await Task.Run(() => Console.WriteLine(5.ToString()));
                                        TResult Func()
                                              ↓
        Console.WriteLine((await Task.Run(() => 6)).ToString());
                                           Task Func()
                                               ↓
        await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString())));
                                      Task<TResult> Func()
                                               ↓
        int value = await Task.Run(() => Task.Run(() => 8));
        Console.WriteLine(value.ToString());
    }
}
class Program
{
    static void Main()
    {
        Task t = MyClass.DoWorkAsync();
        t.Wait();
        Console.WriteLine("Press Enter key to exit");
        Console.Read();
    }
}

輸出:

5
6
7
8
Press Enter key to exit

在能使用任何其餘表達式的地方,均可以使用await表達式(只要位於異步方法內)。在上面的代碼中,4個await表達式用在了3個不一樣的位置。

  • 第一個和第三個實例將await表達式用做語句。
  • 第二個實例將await表達式用做WriteLine方法的參數。
  • 第四個實例將await表達式用做賦值語句的右端。

假設咱們的某個方法不符合這4種委託形式。例如,假設有一個GetSum方法以兩個int值做爲輸入,並返回這兩個值的和。這與上述4個可接受的委託都不兼容。要解決這個問題,能夠用可接受的Func委託的形式建立一個Lambda函數,其惟一的行爲就是運行GetSum方法,以下面的代碼所示:

int value = await Task.Run(()=> GetSum(5,6));

Lambda函數()=>GetSum(5,6)知足Func<TResult>委託,由於它沒有參數,且返回單一的值。
下面的代碼展現了完整的示例:

static class MyClass
{
    private static int GetSum(int i1, int i2)
    {
        return i1+i2;
    }
    public static async Task DoWorkAsync()
    {
        int value=await Task.Run(()=>GetSum(5,6));
        Console.WriteLine(value.ToString());
    }
}
class Program
{
    static void Main()
    {
        Task t = MyClass.DoWorkAsync();
        t.Wait();
        Console.WriteLine("Press Enter key to exit"); 
        Console.Read();
    }
}

輸出:

11
Press Enter key to exit

取消一個異步操做

一些.NET異步方法容許你請求終止執行。你一樣也能夠在本身的異步方法中加入這個特性。
System.Threading.Tasks命名空間中有兩個類是爲此目的而設計的:CancellationTokenCancellationTokenSource

  • CancellationToken對象包含一個任務是否應被取消的信息
  • 擁有CancellationToken對象的任務須要按期檢查其令牌(token)狀態。若是CancellationToken對象的IsCancellationRequested屬性爲true,任務需中止其操做並返回
  • CancellationToken是不可逆的,而且只能使用一次。也就是說,一旦IsCancellationRequested屬性被設置爲true,就不能更改了
  • CancellationTokenSource對象建立可分配給不一樣任務的CancellationToken對象。任何持有CancellationTokenSource的對象均可以調用其Cancel方法,這會將CancellationTokenIsCancellationRequested屬性設置爲true

下面的代碼展現瞭如何使用CancellationTokenSourceCancellationToken來實現取消操做。注意,該過程是協同的。即調用CancellationTokenSourceCancel時,它自己並不會執行取消操做。而是會將CancellationTokenIsCancellationRequested屬件設置爲true。包含CancellationToken的代碼負責檢查該屬性,並判斷是否須要中止執行並返回。
下面的代碼展現瞭如何使用這兩個取消類。以下所示代碼並無取消異步方法,而是在Main方法中間有兩行被註釋的代碼,它們觸發了取消行爲。

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        MyClass mc = new MyClass();
        Task t = mc.RunAsync(token);
        //Thread.Sleep(3000);//等待3秒
        //cts.Cancel();      //取消操做
        t.Wait();
        Console.WriteLine("Was Cancelled: {0}", token.IsCancellationRequested);
        Console.ReadKey();
    }
}
class MyClass
{
    public async Task RunAsync(CancellationToken ct)
    {
        if (ct.IsCancellationRequested)
            return;
        await Task.Run(() => CycleMethod(ct), ct);
    }
    // CycleMethod完全執行完須要5s
    void CycleMethod(CancellationToken ct)
    {
        Console.WriteLine("Starting CycleMethod");
        const int max = 5;
        for (int i = 0; i < max; i++)
        {
            if (ct.IsCancellationRequested)   // 監控CancellationToken
                return;
            Thread.Sleep(1000);
            Console.WriteLine("    {0} of {1} iterations completed", i + 1, max);
        }
    }
}

第一次運行時保留註釋的代碼,不會取消任務,產生的結果以下:

Starting CycleMethod
    1 of 5 iterations completed
    2 of 5 iterations completed
    3 of 5 iterations completed
    4 of 5 iterations completed
    5 of 5 iterations completed
Was Cancelled: False

若是取消Main方法中對Thread.SleepCancel語句的屏蔽,任務將在3秒後取消,產生的結果以下:

Starting CycleMethod
    1 of 5 iterations completed
    2 of 5 iterations completed
    3 of 5 iterations completed
Was Cancelled: True

異常處理和await表達式

能夠像使用其餘表達式那樣,將await表達式放在try語句內,try…catch…finally結構將按你指望的那樣工做。
下面的代碼展現了一個示例,其中await表達式中的任務會拋出一個異常。await表達式位於try塊中,將按普通的方式處理異常。

class Program
{
    static void Main(string[] args)
    {
        Task t = BadAsync();
        t.Wait();
        Console.WriteLine("Task Status : {0}", t.Status);
        Console.WriteLine("Task IsFaulted: {0}", t.IsFaulted);
    }
    static async Task BadAsync()
    {
        try
        {
            await Task.Run(() => { throw new Exception(); });
        }
        catch
        {
            Console.WriteLine("Exception in BadAsync");
        }
    }
}

輸出:

Exception in BadAsync
Task Status : RanToCompletion
Task IsFaulted: False

注意,儘管Task拋出了一個Exception,在Main的最後,Task的狀態仍然爲RanToCompletion。這會讓人感到很意外,由於異步方法拋出了異常。
緣由是如下兩個條件成立:(1)Task沒有被取消,(2)沒有未處理的異常。相似地,IsFaulted屬性爲False,由於沒有未處理的異常。

在調用方法中同步地等待任務

調用方法能夠調用任意多個異步方法並接收它們返回的Task對象。而後你的代碼會繼續執行其餘任務,但在某個點上可能會須要等待某個特殊Task對象完成,而後再繼續。爲此,Task類提供了一個實例方法Wait,能夠在Task對象上調用該方法。
下面的示例展現了其用法。在代碼中,調用方法DoRun調用異步方法CountCharactersAsync並接收其返回的Task<int>。而後調用Task實例的Wait方法,等待任務Task結束。等結束時再顯示結果信息。

static class MyDownloadString
{
    public static void DoRun()
    {
        Task<int> t = CountCharactersAsync("https://www.zhihu.com/");
        //t.Wait();
        Console.WriteLine("The task is executing.");
        Console.WriteLine("The task has finished, returning value {0}.", t.Result);
    }
    private static async Task<int> CountCharactersAsync(string site)
    {
        string result = await new WebClient().DownloadStringTaskAsync(new Uri(site));
        return result.Length;
    }
}
class Program
{
    static void Main()
    {
        MyDownloadString.DoRun();
        Console.ReadKey();
    }
}

輸出:

The task is executing.
The task has finished, returning value 8328.

屏蔽t.Wait();時,先輸出第一句,Task<int> t執行完成後輸出第二句;不屏蔽t.Wait();時,Task<int> t執行完成後同時輸出這兩句。
Wait方法用於單一Task對象。而你也能夠等待一組Task對象。對於一組Task,能夠等待全部任務都結束,也能夠等待某一個任務結束。實現這兩個功能的是Task類中的兩個靜態方法:

  • WaitAll
  • WaitAny

這兩個方法是同步方法且沒有返回值。它們中止,直到條件知足後再繼續執行。
咱們來看一個簡單的程序,它包含一個DoRun方法,兩次調用一個異步方法並獲取其返回的兩個Task<int>對象。而後,方法繼續執行,檢査任務是否完成並打印。
以下所示的程序並無使用等待方法,而是在DoRun方法中間註釋的部分包含等待的代碼,咱們將在稍後用它來與如今的版本進行比較。

class MyDownloadString
{
    Stopwatch sw = new Stopwatch();
    public void DoRun()
    {
        sw.Start();
        Task<int> t1 = CountCharactersAsync( 1, "http://www.microsoft.com");
        Task<int> t2 = CountCharactersAsync( 2, "http://www.illustratedcsharp.com" );
        //Task<int>[] tasks = new Task<int>[]{ t1, t2 };
        //Task.WaitAll( tasks );
        //Task.WaitAny( tasks );
        Console.WriteLine( "Task 1:  {0}Finished", t1.IsCompleted ? "" : "Not ");
        Console.WriteLine( "Task 2:  {0}Finished", t2.IsCompleted ? "" : "Not ");
        Console.Read();
    }
    private async Task<int> CountCharactersAsync( int id, string site )
    {
        WebClient wc = new WebClient();
        string result = await wc.DownloadStringTaskAsync( new Uri( site )); 
        Console.WriteLine(" Call {0} completed:    {1} ms",id, sw.Elapsed.TotalMilliseconds );
        return result.Length;
    }
}
class Program
{
    static void Main()
    {
        MyDownloadString ds = new MyDownloadString();
        ds.DoRun();
    }
}

代碼產生的結果以下。注意,在檢査這兩個TaskIsCompleted方法時,沒有一個是完成的。

Task 1:  Not Finished
Task 2:  Not Finished
 Call 1 completed:    127.3862 ms
 Call 2 completed:    647.7455 ms

若是咱們取消DoRun中間那兩行代碼中第一行的註釋(以下面的三行代碼所示),方法將建立一個包含這兩個任務的數組,並將這個數組傳遞給WaitAll方法。這時代碼會中止並等待任務所有完成,而後繼續執行。

Task<int>[] tasks = new Task<int>[] {t1,t2};
Task.WaitAll( tasks );
//Task.WaitAny( tasks );

此時運行代碼,其結果以下:

 Call 1 completed:    100.8518 ms
 Call 2 completed:    551.1589 ms
Task 1:  Finished
Task 2:  Finished

若是咱們再次修改代碼,註釋掉WaitAll方法調用,取消WaitAny方法調用的註釋,代碼將以下所示:

Task<int>[] tasks = new Task<int>[] {t1,t2};
//Task.WaitAll( tasks );
Task.WaitAny( tasks );

這時,WaitAny調用將終止並等待至少一個任務完成。運行代碼的結果以下:

 Call 1 completed:    158.7846 ms
Task 1:  Finished
Task 2:  Not Finished
 Call 2 completed:    610.8676 ms

WaitAllWaitAny分別還包含4個重載,除了任務完成以外,還容許其餘繼續執行的方式,如設置超時時間或使用CancellationToken來強制執行處理的後續部分。下表展現了這些重載方法。

在異步方法中異步地等待任務

上節學習瞭如何同步地等待Task完成。但有時在異步方法中,你會但願用await表達式來等待Task。這時異步方法會返回到調用方法,但該異步方法會等待一個或全部任務完成。能夠經過Task.WhenAllTask.WhenAny方法來實現。這兩個方法稱爲組合子(combinator)。
下面的代碼展現了一個使用Task.WhenAll方法的示例。它異步地等待全部與之相關的Task完成,不會佔用主線程的時間。注意,await表達式的任務就是調用Task.WhenAll

using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
class MyDownloadString
{
    public void DoRun()
    {
        Task<int> t = CountCharactersAsync( "http://www.microsoft.com",
                                            "http://www.illustratedcsharp.com");
        Console.WriteLine( "DoRun: Task {0}Finished", t.IsCompleted ?  "": "Not " );
        Console.WriteLine( "DoRun: Result = {0}", t.Result );
    }
    private async Task<int> CountCharactersAsync(string sitel, string site2)
    {
        WebClient wcl = new WebClient();
        WebClient wc2 = new WebClient();
        Task<string> t1 = wcl.DownloadStringTaskAsync( new Uri( sitel ));
        Task<string> t2 = wc2.DownloadStringTaskAsync( new Uri( site2 ));
        List<Task<string>> tasks = new List<Task<string>>();
        tasks.Add( t1 );
        tasks.Add( t2 );
        await Task.WhenAll( tasks );
        Console.WriteLine("    CCA: T1 {0}Finished", t1.IsCompleted ? "" : "Not ");
        Console.WriteLine("    CCA: T2 {0}Finished", t2.IsCompleted ? "" : "Not ");
        return t1.IsCompleted ? t1.Result.Length : t2.Result.Length;
    }
}
class Program
{
    static void Main()
    {
        var ds=new MyDownloadString();
        ds.DoRun();
    }
}

輸出:

DoRun: Task Not Finished
    CCA: T1 Finished
    CCA: T2 Finished
DoRun: Result = 1020

Task.WhenAny組合子會異步地等待與之相關的某個Task完成。若是將上面的await表達式由調用Task.WhenAll改成調用Task.WhenAny,並返回到程序,將產生如下輸出結果:

DoRun: Task Not Finished
    CCA: T1 Finished
    CCA: T2 Not Finished
DoRun: Result = 1020

Task.Delay方法

Task.Delay方法建立一個Task對象,該對象將暫停其在線程中的處理,並在必定時間以後完成。然而與Thread.Sleep阻塞線程不一樣的是,Task.Delay不會阻塞線程,線程能夠繼續處理其餘工做。
下面的代碼展現瞭如何使用Task.Delay方法:

class Simple
{
    Stopwatch sw = new Stopwatch();
    public void DoRun()
    {
        Console.WriteLine( "Caller: Before call");
        ShowDelayAsync();
        Console.WriteLine( "Caller: After call");
    }
    private async void ShowDelayAsync()
    {
        sw.Start();
        Console.WriteLine( " Before Delay: {0}", sw.ElapsedMilliseconds );
        await Task.Delay( 1000 );
        Console.WriteLine( " After Delay : {0}", sw.ElapsedMilliseconds );
    }
}
class Program
{
    static void Main()
    {
        var ds = new Simple ();
        ds.DoRun();
        Console.Read();
    }
}

輸出:

Caller: Before call
 Before Delay: 0
Caller: After call
 After Delay : 1013

Delay方法包含4個重載,能夠以不一樣方式來指定時間週期,同時還容許使用CancellationToken對象。下表展現了該方法的4個重載。

在GUI程序中執行異步操做


儘管本章目前的全部代碼均爲控制檯應用程序,但實際上異步方法在GUI程序中尤其有用。
緣由是GUI程序在設計上就要求全部的顯示變化都必須在主GUI線程中完成,如點擊按鈕、展現標籤、移動窗體等。Windows程序是經過消息來實現這一點的,消息被放入由消息泵管理的消息隊列中。
消息泵從隊列中取出一條消息,並調用它的處理程序(handler)代碼。當處理程序代碼完成時,消息泵獲取下一條消息並循環這個過程。
因爲這種架構,處理程序代碼就必須快捷,這樣纔不至於掛起並阻礙其餘GUI行爲的處理。若是某個消息的處理程序代碼耗時過長,消息隊列中的消息會產生積壓。程序將失去響應,由於在那個長時間運行的處理程序完成以前,沒法處理任何消息。

下圖展現了一個WPF程序中兩個版本的窗體。窗體由狀態標籤及其下方的按鈕組成。開發者的目的是,程序用戶點擊按鈕,按鈕的處理程序代碼執行如下操做:

  • 禁用按鈕,這樣在處理程序執行期間用戶就不能再次點擊了
  • 將標籤文本改成Doing Stuff,這樣用戶就會知道程序正在工做
  • 讓程序休眠4秒鐘——模擬某個工做
  • 將標籤文本改成原始文本,並啓用按鈕。

右圖的截屏展現了開發者但願在按鈕按下的4秒以內窗體的樣子。然而事實並不是如此。當開發者點擊按鈕後,什麼都沒有發生。並且若是在點擊按鈕後移動窗體,會發現它已經凍結,不會移動——直到4秒以後,窗體才忽然出如今新位置。

注意 WPF是微軟替代Windows Form的GUI編程框架。要了解更多關於WPF編程的知識,請參閱筆者的 Illustrated WPF(Apress,2009)一書。

要使用Visual Studio 2012建立這個名爲MessagePump的WPF程序,步驟以下:

1.選擇File→New→Project菜單項,彈出New Project窗口
2.在窗口左側的面板內,展開Installed Templates(若是沒有展開的話)
3.在C#類別中點擊Windows條目,將在中間面板中彈出已安裝的Windows程序模板
4.點擊WPF Application,在窗口下方的Name文本框中輸人MessagePump。在其下方選擇一個位置,並點擊OK按鈕
5.將MainWindow.xaml中的XAML標記修改成下面的代碼,在窗體中建立狀態標籤和按鈕。

<Window x:Class="MessagePump.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Pump" Height="120" Width="200">
    <StackPanel>
        <Label Name="lblStatus" Margin="10,5,10,0" >Not Doing Anything</Label>
        <Button Name="btnDoStuff" Content="Do Stuff" HorizontalAlignment="Left"
                Margin="10,5" Padding="5,2" Click="btnDoStuff_Click"/>
    </StackPanel>
</Window>

6.將代碼隱藏文件MainWindow.xaml.cs修改成以下C#代碼。

using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace MessagePump
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void btnDoStuff_Click( object sender, RoutedEventArgs e )
        {
            btnDoStuff.IsEnabled = false;
            lblStatus.Content = "Doing Stuff";
            Thread.Sleep( 4000 );
            lblStatus.Content   = "Not Doing Anything";
            btnDoStuff.IsEnabled = true;
        }
    }
}

運行程序,你會發現其行爲與以前的描述徹底一致,即按鈕沒有禁用,狀態標籤也沒有改變,在4秒以內窗體也沒法移動。
這個奇怪行爲的緣由其實很是簡單。下圖展現了這種情形。點擊按鈕時,按鈕的Click消息放入消息隊列。消息泵從隊列中移除該消息並開始處理點擊按鈕的處理程序代碼,即btnDoStuff_Click方法。btnDoStuff_Click處理程序將咱們但願觸發的行爲的消息放入隊列,以下右圖所示。但在處理程序自己退出(即休眠4秒並退出)以前,這些消息都沒法執行。而後全部的行爲都發生了,但速度太快肉眼根本看不見。

可是,若是處理程序能將前兩條消息壓入隊列,而後將本身從處理器上摘下,在4秒以後再將本身壓入隊列,那麼這些以及全部其餘消息均可以在等待的時間內被處理,整個過程就會如咱們以前預料的那樣,而且還能保持響應。
咱們可使用async/await特性輕鬆地實現這一點,以下面修改的處理程序代碼。當到達await語句時,處理程序返回到調用方法,並從處理器上摘下。這時其餘消息得以處理——包括處理程序已經壓入隊列的那兩條。在空閒任務完成後(本例中爲Task.Delay),後續部分(方法剩餘部分)又被從新安排到線程上。

private async void btnDoStuff_Click(object sender, RoutedEventArgs e )
{
    btnDoStuff.IsEnabled = false;
    lblStatus.Content = "Doing Stuff";
    await Task.Delay( 4000 );
    lblStatus.Content = "Not Doing Anything";
    btnDoStuff.IsEnabled = true;
}

Task.Yield

Task.Yield方法建立一個當即返回的awaitable。等待一個Yield可讓異步方法在執行後續部分的同時返回到調用方法。能夠將其理解成離開當前的消息隊列,回到隊列末尾,讓處理器有時間處理其餘任務。
下面的示例代碼展現了一個異步方法,程序每執行某個循環1000次就移交一次控制權。每次執行Yield方法,都會容許線程中的其餘任務得以執行。

static class DoStuff
{
    public static async Task<int> FindSeriesSuw( int il )
    {
        int sum = 0;
        for ( int i=0; i < il; i++ )
        {
            sum += i;
            if ( i % 1000 == 0 )
                await Task.Yield();
        }
        return sum;
    }
}
class Program
{
    static void Main()
    {
        Task<int> value = DoStuff.FindSeriesSuw( 1000000 );
        CountBig( 100000 ); CountBig( 100000 );
        CountBig( 100000 ); CountBig( 100000 );
        Console.WriteLine( "Sum: {0}", value.Result );
        Console.ReadKey();
    }
    private static void CountBig( int p )
    {
        for ( int i=0; i < p; i++)
            ;
    }
}

輸出:

Sum: 1783293664

Yield方法在GUI程序中很是有用,能夠中斷大量工做,讓其餘任務使用處理器

使用異步Lambda表達式


到目前爲止,本章只介紹了異步方法。但我曾經說過,你還可使用異步匿名方法和異步Lambda表達式。這種構造尤爲適合那些只有不多工做的事件處理程序。下面的代碼片斷將一個Lambda表達式註冊爲一個按鈕點擊事件的事件處理程序。

startWorkButton.Click += async (sender,e )=>
{
    //處理點擊處理程序工做
}

下面用一個簡短的WPF程序來展現其用法,下面爲後臺代碼:

using System.Threading.Tasks;
using System.Windows;
namespace AsyncLambda
{
    public partial class MainMindow : Window
    {
        public MainMindow()
        {
            InitializdComponent();
            startWorkButton.Click += async (sender,e)=>
            {
                SetGuiValues( false, "Work Started");
                await DoSomeWork();
                SetGuiValues( true, "Work Finished");
            };
        }
        private void SetGuiValues(bool buttonEnabled, string status)
        {
            startWorkButton.IsEnabled = buttonEnabled;
            workStartedTextBlock.Text = status;
        }
        private Task DoSomeWork()
        {
            return Task.Delay(2500);
        }
    }
}

XAML文件中的標記以下:

<Window x:Class="AsyncLambda.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Async Lambda" Height="115" Width="150">
    <StackPanel>
        <TextBlock Name="workStartedTextBlock" Margin="10,10"/>
        <Button Name="startWorkButton" Content="Start Work" Width="100" Margin="4"/>
    </StackPanel>
</Window>

完整的GUI程序

咱們按部就班地介紹了async/await組件。本節你將看到一個完整的WPF GUI程序,包含一個狀態條和取消操做。
以下圖所示,左邊爲示例程序的截圖。點擊按鈕,程序將開始處理並更新進度條。處理過程完成將顯示右上角的消息框。若是在處理完成前點擊Cancel按鈕,程序將顯示右下角的消息框。

咱們首先建立一個名爲WpfAwait的WPF應用程序。按以下的代碼修改MainWindow.xaml中的XAML標記:

<Window x:Class="WpfAwait.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Process and Cancel" Height="150" Width="250">
    <StackPanel>
        <Button Name="btnProcess" Width="100" Click="btnProcess_Click"
                HorizontalAlignment="Right" Margin="10,15,10,10">Process</Button>
        <Button Name="btnCancel" Width="100" Click="btnCancel_Click"
                HorizontalAlignment="Right" Margin="10,0">Cancel</Button>
        <ProgressBar Name="progressBar" Height="20" Width="200" Margin="10"
                     HorizontalAlignment="Right"/>
    </StackPanel>
</Window>

按以下的代碼修改後臺代碼文件MainWindow.xaml.cs:

using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace WpfAwait
{
    public partial class MainMindow : Window
    {
        CancellationTokenSource _cancellationTokenSource;
        CancellationToken   _cancellationToken;
        public MainMindow()
        {
            InitializeComponent();
        }
        private async void btnProcess_Click( object sender, RoutedEventArgs e )
        {
            btnProcess.IsEnabled = false;
            _cancellationTokenSource = new CancellationTokenSource();
            _cancellationToken = _cancellationTokenSource.Token;
            int completedPercent = 0;
            for ( int i = 0; i < 10; i++)
            {
                if ( _cancellationToken.IsCancellationRequested )
                    break;
                try
                {
                    await Task.Delay( 500, _cancellationToken );
                    completedPercent =( i + 1 ) * 10;
                }
                catch ( TaskCanceledException ex )
                {
                    completedPercent = i * 10;
                }
                progressBar.Value = completedPercent;
            }
            string message = _cancellationToken.IsCancellationRequested
                            ? string.Format("Process was cancelled at {0}%.", completedPercent) :"Process completed normally.";
            MessageBox.Show( message, "Completion Status");
            progressBar.Value = 0;
            btnProcess.IsEnabled = true;
            btnCancel.IsEnabled = true;
        }
        private void btnCancel_Click( object sender, RoutedEventArgs e )
        {
            if ( !btnProcess.IsEnabled )
            {
                btnCancel.IsEnabled = false;
                _cancellationTokenSource.Cancel();
            }
        }
    }
}

BackgroundWorker類


前面幾節介紹瞭如何使用async/await特性來異步地處理任務。本節將學習另外一種實現異步工做的方式——即後臺線程。async/await特性更適合那些須要在後臺完成的不相關的小任務。
但有時候,你可能須要另建一個線程,在後臺持續運行以完成某項工做,並不時地與主線程進行通訊。BackgroundWorker類就是爲此而生。下圖展現了此類的主要成員。

  • 圖中一開始的兩個屬性用於設置後臺任務是否能夠把它的進度彙報給主線程以及是否支持從主線程取消。能夠用第三個屬性來檢査後臺任務是否正在運行
  • 類有三個事件,用於發送不一樣的程序事件和狀態。你須要爲本身的程序寫這些事件的事件處理方法來執行適合程序的行爲
    • 在後臺線程開始的時候觸發DoWork
    • 在後臺任務彙報狀態的時候觸發ProgressChanged事件
    • 後臺工做線程退出的時候觸發RunWorkerCompleted事件
  • 三個方法用於初始化行爲或改變狀態
    • 調用RunWorkerAsync方法獲取後臺線程而且執行DoWork事件處理程序
    • 調用CancelAsync方法把CancellationPending屬性設置爲trueDoWork事件處理程序須要檢查這個屬性來決定是否應該中止處理
    • DoWork事件處理程序(在後臺線程)在但願向主線程彙報進度的時候,調用ReportProgress方法

要使用BackgroundWorker類對象,須要寫以下的事件處理程序。第一個是必需的,由於它包含你但願在後臺線程執行的代碼,另外兩個是可選的,是否使用取決於程序須要。

  • 附加到DoWork事件的處理程序包含你但願在後臺獨立線程上執行的代碼。
    • 在下圖中,叫作DoTheWork的處理程序用漸變的方塊表示,代表它在獨立的線程中執行
    • 主線程調用RunWorkerAsync方法的時候觸發DoWork事件
  • 這個後臺線程經過調用ReportProgress方法與主線程通訊。屆時將觸發ProgressChanged事件,主線程能夠處理附加到ProgressChanged事件上的處理程序
  • 附加到RunWorkerCompleted事件的處理程序應該包含後臺線程完成DoWork亊件處理程序的執行以後須要執行的代碼。

下演示了程序的結構,以及附加到BackgroundWorker對象事件的事件處理程序。

這些事件處理程序的委託以下。每個任務都有一個object對象的引用做爲第一個參數,以及EventArgs類的特定子類做爲第二個參數。

void DoWorkEventHandler             ( object sender, DoWorkEventArgs e )
void ProgressChangedEventHandler    ( object sender, ProgressChangedEventArgs e )
void RunWorkerCompletedEventHandler ( object sender, RunWorkerCompletedEventArgs e)

下圖演示了這些事件處理程序的EventArg類的結構。

若是你編寫了這些事件處理程序並將其附加到相應的事件,就能夠這樣使用這些類。

  • 從建立BackgroundWorker類的對象而且對它進行配置開始
    • 若是但願工做線程爲主線程回報進度,須要把WorkerReportsProgress屬性設置爲true
    • 若是但願從主線程取消工做線程,就把WorkerSupportsCancellation屬性設置爲true
    • 既然對象已經配置好了,咱們就能夠經過調用RunWorkerAsync方法來啓動它。它會開一個後臺線程而且發起DoWork事件並在後臺執行事件處理程序

如今咱們已經運行了主線程以及後臺線程。儘管後臺線程正在運行,你仍然能夠繼續主線程的處理。
在主線程中,若是你已經啓用了WorkerSupportsCancellation屬性,而後能夠調用對象的CancelAsync方法。和本章開頭介紹的CancellationToken同樣,它也不會取消後臺線程。而是將對象的CancellationPending屬性設置爲true。運行在後臺線程中的DoWork事件處理程序代碼須要按期檢査CancellationPending屬性,來判斷是否須要退出。
同時在後臺線程繼續執行其計算任務,而且作如下幾件事情。

  • 若是WorkerReportsProgress屬性是true而且後臺線程須要爲主線程彙報進度的話,必須調用BackgroundWorker對象的ReportProgress方法。這會觸發主線程的ProgressChanged事件,從而運行相應的事件處理程序
  • 若是WorkerSupportsCancellation屬性啓用的話,DoWork事件處理程序代碼應該常常檢測CancellationPending屬性來肯定是否已經取消了。若是是的話,則應該退出
  • 若是後臺線程沒有取消完成了其處理的話,能夠經過設置DoWorkEventArgs參數的Result字段來返回結果給主線程,這在上圖中已經說過了。

在後臺線程退出的時候會觸發RunWorkerCompleted事件,其事件處理程序在主線程上執行。RunWorkerCompletedEventArgs參數能夠包含已完成後臺線程的一些信息,好比返回值以及線程是否被取消了。

在WPF程序中使用BackgroundWorker類的示例代碼

BackgroundWorker類主要用於GUI程序,下面的程序展現了一個簡單的WPF程序。
該程序會生成下圖中左圖所示的窗體。點擊Process按鈕將開啓後臺線程,每半秒向主線程報告一次,並使進度條增加10%。最終,將展現右圖所示的對話框。

要建立這個WPF程序,須要在Visual Studio中建立名爲SimpleWorker的WPF應用程序。將MainWindow.xaml文件中的代碼修改成:

<Window x:Class="SimpleWorker.MainMindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="l50" Width="250">
    <StackPanel>
        <ProgressBar Name="progressBar" Height="20" Width="200" Margin="10"/>
        <Button Name="btnProcess" Width="l00" Click="btnProcess_Click" Margin="5">Process</Button>
        <Button Name="btnCancel" Width="l00" Click="btnCancel_Click" Margin="5">Cancel</Button>
    </StackPanel>
</Window>

將MainWindow.xaml.cs文件中的代碼修改成:

using System.Windows;
using System.ComponentModel;
using System.Threading;
namespace SimpleWorker
{
    public partial class MainWindow : Window
    {
        BackgroundWorker bgWorker = new BackgroundWorker();
        public MainWindow()
        {
            InitializeComponent();
            //設置BackgroundWorker 屬性
            bgWorker.WorkerReportsProgress = true;
            bgWorker.WorkerSupportsCancellation = true;
            //鏈接BackgroundWorker對象的處理程序
            bgWorker.DoWork +=  DoWork_Handler;
            bgWorker.ProgressChanged += ProgressChanged_Handler;
            bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler;
        }
        private void btnProcess_Click( object sender, RoutedEventArgs e)
        {
            if ( !bgWorker.IsBusy )
                bgWorker.RunWorkerAsync();
        }
        private void ProgressChanged_Handler( object sender,ProgressChangedEventArgs args )
        {
            progressBar.Value = args.ProgressPercentage;
        }
        private void DoWork_Handler( object sender, DoWorkEventArgs args )
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            for ( int i = 1; i <= 10; i++ )
            {
                if ( worker.CancellationPending )
                {
                    args.Cancel = true; 
                    break;
                }
                else
                {
                    worker.ReportProgress( i * 10 );
                    Thread.Sleep( 500 );
                }
            }
        }
        private void RunWorkerCompleted_Handler( object sender,RunWorkerCompletedEventArgs args )
        {
            progressBar.Value = 0;
            if ( args.Cancelled )
                MessageBox.Show( "Process was cancelled.", "Process Cancelled");
            else
                MessageBox.Show( "Process completed normally.", "Process Completed" );
        }
        private void btnCancel_Click( object sender, RoutedEventArgs e )
        {
            bgWorker.CancelAsync();
        }
    }
}

並行循環


本節將簡要介紹任務並行庫(Task Parellel Library)。它是BCL中的一個類庫,極大地簡化了並行編程。其細節比本章要介紹的多得多。因此,我在這裏只能經過介紹其中的兩個簡單的結構做爲開胃菜了,這樣你能夠快速並很容易地入門,它們是Parallel.For循環和Parallel.ForEach循環。這兩個結構位於System.Threading.Tasks命名空間中。
至此,我相信你應該很熟悉C#的標準for和foreach循環了。這兩個結構很是廣泛,且極其強大。許多時候咱們的循環結構的每一次迭代依賴於以前那一次迭代的計算或行爲。但有的時候又不是這樣。若是迭代之間彼此獨立,而且程序運行在多核處理器的機器上,若能將不一樣的迭代放在不一樣的處理器上並行處理的話,將會獲益匪淺。Parallel.ForParallel.ForEach結構就是這樣作的。
這些構造的形式是包含輸入參數的方法。Parallel.For方法有12個重載,最簡單的簽名以下。

public static ParallelLoopResult.For( int fromInclusive, int toExclusive, Action body);
  • fromInclusive參數是迭代系列的第一個整數
  • toExclusive參數是比迭代系列最後一個索引號大1的整數。也就是說,和表達式index<ToExclusive—樣
  • body是接受單個輸入參數的委託,body的代碼在每一次迭代中執行一次

以下代碼是使用Parallel.For結構的例子。它從0到14迭代(記住實際的參數15超出了最大迭代索引)而且打印出迭代索引和索引的平方。該應用程序知足各個迭代之間是相互獨立的條件。還要注意,必須使用System.Threading.Tasks命名空間。

using System;
using System.Threading.Tasks;   // 必須使用這個命名空間
namespace ExampleParallelFor
{
    class Program
    {
        static void Main()
        {
            Parallel.For(0,15,i=>
                Console.WriteLine("The square of {0} is {1}",i,i*i));
        }
    }
}

在一個四核處理器的PC上運行這段代碼產生以下輸出。注意,不能確保迭代的執行次序。

The square of 0 is 0
The square of 6 is 36
The square of 7 is 49
The square of 8 is 64
The square of 10 is 100
The square of 11 is 121
The square of 13 is 169
The square of 14 is 196
The square of 4 is 16
The square of 5 is 25
The square of 1 is 1
The square of 2 is 4
The square of 9 is 81
The square of 12 is 144
The square of 3 is 9

另外一個示例以下。程序以並行方式填充一個整數數組,把值設置爲迭代索引號的平方。

class Program
{
    static void Main()
    {
        const int maxValues=50;
        int[] squares=new int[maxValues];
        Parallel.For(0,maxValues,i=>squares[i]=i*i);
    }
}

在本例中,即便迭代在執行時可能爲並行而且爲任意順序。可是最後結果始終是一個包含前50個平方數的數組——而且按順序排列。
另一個並行循環結構是Parallel.ForEach方法。該方法有至關多的重載,其中最簡單的以下:

  • TSource是集合中對象的類型
  • source是一組TSource對象的集合
  • body是要應用到集合中每個元素的Lambda表達式
static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source,Action<TSource> body)

使用Paralle.ForEach方法的例子以下。在這裏,TSourcestringsourcestring[]

using System;
using System.Threading.Tasks;
namespace ParallelForeach1
{
    class Program
    {
        static void Main()
        {
            string[] squares=new string[] {"We","hold","these","truths","to","be","self-evident","that","all","men","are","created","equal"};
            Parallel.ForEach(squares,
                i=>Console.WriteLine(string.Format("{0} has {1} letters",i,i.Length)));
        }
    }
}

在一個四核處理器的PC上運行這段代碼產生以下輸出,可是每一次運行均可能會有不同的順序。

We has 2 letters
men has 3 letters
truths has 6 letters
self-evident has 12 letters
equal has 5 letters
are has 3 letters
created has 7 letters
to has 2 letters
be has 2 letters
hold has 4 letters
these has 5 letters
that has 4 letters
all has 3 letters

其餘異步編程模式


若是咱們要本身編寫異步代碼,最可能使用的就是本章前面介紹的async/await特性和BackgroundWorker類,或者任務並行庫。然而,你仍然有可能須要使用舊的模式來產生異步代碼。爲了保持完整性,我將從如今開始介紹這些模式,直到本章結束。在學習了這些舊模式後,你將對async/await特性是多麼簡單有更加深入的認識
第13章介紹了委託的主題,而且瞭解到當委託對象調用時,它調用了它的調用列表中包含的方法。就像程序調用方法同樣,這是同步完成的。
若是委託對象在調用列表中只有一個方法(以後會叫作引用方法),它就能夠異步執行這個方法。委託類有兩個方法,叫作BeginInvokeEndInvoke,它們就是用來這麼作的。這些方法以以下方式使用。

  • 當咱們調用委託的BeginInvoke方法時,它開始在一個獨立線程上執行引用方法,而且當即返回到原始線程。原始線程能夠繼續,而引用方法會在線程池的線程中並行執行
  • 當程序但願獲取已完成的異步方法的結果時,能夠檢查BeginInvoke返回的IAsyncResultIsCompleted屬性,或調用委託的EndInvoke方法來等待委託完成

下圖演示了使用這一過程的三種標準模式。對於這三種模式來講,原始線程都發起了一個異步方法,而後作一些其餘處理。然而,這些模式的區別在於,原始線程如何知道發起的線程已經完成。

  • 在等待一直到完成(wait-until-done )模式中,在發起了異步方法以及作了一些其餘處理以後,原始線程就中斷而且等異步方法完成以後再繼續
  • 在輪詢(polling )模式中,原始線程按期檢查發起的線程是否完成,若是沒有則能夠繼續作一些其餘的事情
  • 在回調(callback)模式中,原始線程一直執行,無需等待或檢査發起的線程是否完成。在發起的線程中的引用方法完成以後,發起的線程就會調用回調方法,由回調方法在調用EndInvoke以前處理異步方法的結果

BeginInvoke 和 EndInvoke


在學習這些異步編程模式的示例以前,讓咱們先研究一下BeginInvokeEndInvoke方法。一些須要瞭解的有關BeginInvoke的重要事項以下。

  • 在調用BeginInvoke時,參數列表中的實際參數組成以下
    • 引用方法須要的參數
    • 兩個額外的參數——callback參數和state參數
  • BeginInvoke從線程池中獲取一個線程而且讓引用方法在新的線程中開始運行
  • BeginInvoke返回給調用線程一個實現IAsyncResult接口的對象的引用。這個接口引用包含了在線程池線程中運行的異步方法的當前狀態,原始線程而後能夠繼續執行。

以下的代碼給出了一個調用委託的BeginInvoke方法的示例。第一行聲明瞭MyDel委託類型。下一行聲明瞭一個和委託匹配的Sum的方法。

  • 以後的行聲明瞭一個叫作delMyDel委託類型的委託對象,而且使用Sum方法來初始化它的調用列表
  • 最後一行代碼調用了委託對象的BeginInvoke方法而且提供了兩個委託參數3和5,以及兩個BeginInvoke的參數callbackstate,在本例中都設爲null。執行後,BeginInvoke方法進行兩個操做
    • 從線程池中獲取一個線程而且在新的線程上開始運行Sum方法,將3和5做爲實參
    • 它收集新線程的狀態信息而且把IAsyncResult接口的引用返回給調用線程來提供這些信息。調用線程把它保存在一個叫作iar的變量中
delegate long MyDel(int first,int second);//委託聲明
...
static long Sum(int x,int y){...}         //方法匹配委託
...
MyDel del=new MyDel(Sum);
IAsyncResult iar=del.BeginInvoke(3,5,null,null);

EndInvoke方法用來獲取由異步方法調用返回的值,而且釋放線程使用的資源。EndInvoke有以下的特性。

  • 它接受一個由BeginInvoke方法返回的IAsyncResult對象的引用,並找到它關聯的線程
  • 若是線程池的線程已經退出,EndInvoke作以下的事情
    • 它清理退出線程的狀態而且釋放其資源
    • 它找到引用方法返回的值而且把它做爲返回值
  • 若是當EndInvoke被調用時線程池的線程仍然在運行,調用線程就會中止並等待,直到清理完畢並返回值。由於EndInvoke是爲開啓的線程進行清理,因此必須確保對每個BeginInvoke都調用EndInvoke
  • 若是異步方法觸發了異常,在調用EndInvoke時會拋出異常

以下的代碼行給出了一個調用EndInvoke並從異步方法獲取值的示例。咱們必須把IAsyncResult對象的引用做爲參數。

          委託對象
             ↓
long result=del.EndInvoke(iar);
    ↑                      ↑
異步方法返回值          IAsyncResult對象

EndInvoke提供了從異步方法調用的全部輸出,包括refout參數。若是委託的引用方法有refout參數,它們必須包含在EndInvoke的參數列表中,而且在IAsyncResult對象引用以前,以下所示:

long result=del.EndInvoke(out someInt,iar);
    ↑                         ↑        ↑
異步方法返回值               Out參數 IAsyncResult對象

等待一直到結束模式

既然咱們已經理解了BeginInvokeEndInvoke方法,那麼就讓咱們來看看異步編程模式吧。
咱們要學習的第一種異步編程模式是等待一直到結束模式。在這種模式裏,原始線程發起一個異步方法的調用,作一些其餘處理,而後中止並等待,直到開啓的線程結束。它總結以下:

IAsyncResult iar = del.BeginInvoke( 3, 5, null, null );
//在發起線程中異步執行方法的同時,
//在調用線程中處理一些其餘事情
...
long result = del.EndInvoke( iar );

以下代碼給出了一個使用這種模式的完整示例。代碼使用Thread類的Sleep方法將它本身掛起0.1秒。Thread類在System.Threading命名空間下。

using System;
using System.Threading;  // Thread.Sleep()
delegate long MyDel( int first, int second );   //聲明委託類型
class Program
{
    static long Sum(int x, int y) //聲明異步方法
    {
        Console. WriteLine("                Inside Sum");
        Thread.Sleep(100);
        return x + y;
    }
    static void Main( )
    {
        MyDel del = new MyDel(Sum);
        Console.WriteLine( "Before BeginInvoke");
        IAsyncResult iar = del.BeginInvoke(3, 5, null, null); //開擡異步調用
        Console.WriteLine( "After BeginInvoke");
        Console.WriteLine( "Doing stuff" ); 
        long result = del.EndInvoke( iar ); //等待結果並獲取結果
        Console.WriteLine( "After EndInvoke: {0}", result );
    }
}

等待一直到結束(wait-until-done)模式的輸出:

Before BeginInvoke
After BeginInvoke
Doing stuff
                Inside Sum
After EndInvoke: 8

AsyncResult類

既然咱們已經看到了BeginInvokeEndInvoke的最簡單形式,是時候來進一步接觸IASyncResult了。它是使用這些方法的必要部分。
BeginInvoke返回一個IASyncResult接口的引用(內部是AsyncResult類的對象)。AsyncResult類表現了異步方法的狀態。下圖演示了該類中的一些重要部分。

有關該類的重要事項以下。

  • 當咱們調用委託對象的BeginInvoke方法時,系統建立了一個AsyncResult類的對象。然而,它不返回類對象的引用,而是返回對象中包含的IAsyncResult接口的引用
  • AsyncResult對象包含一個叫作AsyncDelegate的屬性,它返回一個指向被調用來開啓異步方法的委託的引用。可是,這個屬性是類對象的一部分而不是接口的一部分
  • IsCompleted屬性返回一個布爾值,表示異步方法是否完成
  • AsyncState屬性返回一個對象的引用,做爲BeginInvoke方法調用時的state參數。它返回object類型的引用,咱們會在回調模式一節中解釋這部份內容

輪詢模式

在輪詢模式中,原始線程發起了異步方法的調用,作一些其餘處理,而後使用IAsyncResult對象的IsComplete屬性來按期檢査開後的線程是否完成。若是異步方法已經完成,原始線程就調用EndInvoke並繼續。不然,它作一些其餘處理,而後過一下子再檢査。在下面的示例中,「處理」 僅僅是由0數到10 000 000。

delegate long MyDel(int first, int second);
class Program
{
    static long Sum(int x, int y)
    {
        Console.WriteLine("               Inside Sum");
        Thread.Sleep(100);
        return x + y;
    }
    static void Main()
    {
        MyDel del = new MyDel(Sum);發起異步方法
                                        ↓
        IAsyncResult iar = del.BeginInvoke(3, 5, null, null); //開始異步謂用
        Console.WriteLine("After BeginInvoke");
        檢查異步方法是否完成
                    ↓
        while ( !iar.IsCompleted )
        {
            Console.WriteLine("Not Done");
            //繼續處理
            for (long i = 0; i < 10000000; i++)
                ;
        }
        Console.WriteLine("Done");
            調用EndInvoke來獲取接口並進行清理
                            ↓
        long result = del.EndInvoke(iar);
        Console.WriteLine("Result: {0}", result);
    }
}

輪詢(polling)模式的輸出:

After BeginInvoke
Not Done
               Inside Sum
Not Done
Not Done
Not Done
Not Done
Done
Result: 8

回調模式


在以前的等待一直到結束(wait-until-done)模式以及輪詢(polling)模式中,初始線程繼續它本身的控制流程,直到它知道開啓的線程已經完成。而後,它獲取結果並繼續。
回調模式的不一樣之處在於,一旦初始線程發起了異步方法,它就本身管本身了,再也不考慮同步。當異步方法調用結束以後,系統調用一個用戶自定義的方法來處理結果,而且調用委託的EndInvoke方法。這個用戶自定義的方法叫作回調方法或回調
BeginInvoke的參數列表中最後的兩個額外參數由回調方法使用。

  • 第一個參數callback,是回調方法的名字
  • 第二個參數state,能夠是null或要傳入回調方法的一個對象的引用。咱們能夠經過使用IAsyncResult參數的AsyncState屬性來獲取這個對象,參數的類型是object
回調方法

回調方法的簽名和返回類型必須和AsyncCallback委託類型所描述的形式一致。它須要方法接受一個IAsyncResult做爲參數而且返回類型是void,以下所示:

void AsyncCallback( IAsyncResult iar )

咱們有多種方式能夠爲BeginInvoke方法提供回調方法。因爲BeginInvoke中的callback參數是AsyncCallback類型的委託,咱們能夠以委託形式提供,以下面的第一行代碼所示。或者,咱們也能夠只提供回調方法名稱,讓編譯器爲咱們建立委託,兩種形式是徹底等價的。

                                        使用回調方法建立委託
                                                 ↓
IAsyncResult iar1 =del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null);
                                       只須要用回調方法的名字
                                               ↓
IAsyncResult iar2 = del.BeginInvoke(3, 5, CallWhenDone, null);

BeginInvoke的另外一個參數是發送給回調方法的對象。它能夠是任何類型的對象,可是參數類型是object,因此在回調方法中,咱們必須轉換成正確的類型。

在回調方法內調用EndInvoke

在回調方法內,咱們的代碼應該調用委託的EndInvoke方法來處理異步方法執行後的輸出值。要調用委託的EndInvoke方法,咱們確定須要委託對象的引用,而它在初始線程中,不在開啓的線程中。
若是不使用BeginInvokestate參數做其餘用途,可使用它發送委託的引用給回調方法,以下所示:

結合後面的實例看,不將委託對象做爲參數傳入也能夠在回調函數內部獲取AsyncResult類對象。
這樣看來這個位置更應該傳入須要在回調函數中處理或用到的其它對象。

                 委託對象                      把委託對象做爲狀態參數
                    ↓                                    ↓
IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, del);

而後,咱們能夠從發送給方法做爲參數的IAsyncResult對象中提取出委託的引用。以下面的代碼所示。

  • 給回調方法的參數只有一個,就是剛結束的異步方法的IAsyncResult接口的引用。請記住,IAsyncResult接口對象在內部就是AsyncResult類對象
  • 儘管IAsyncResult接口沒有委託對象的引用,而封裝它的AsyncResult類對象卻有委託對象的引用。因此,示例代碼方法體的第一行就經過轉換接口引用爲類類型來獲取類對象的引用。變量ar如今就有類對象的引用
  • 有了類對象的引用,咱們如今就能夠調用類對象的AsyncDelegate屬性而且把它轉化爲合適的委託類型。這樣就獲得了委託引用,咱們能夠用它來調用EndInvoke
using System.Runtime.Remoting.Messaging;  //包含AsyncResult類
void CallWhenDone( IAsyncResult iar )
{
    AsyncResult ar = (AsyncResult) iar;
    MyDel del = (MyDel) ar.AsyncDelegate; //獲取委託的引用
    long Sum = del.EndInvoke( iar );      //調用 EndInvoke
    ...
}

下面把全部知識點放在一塊兒,給出一個使用回調模式的完整示例。

using System;
using System.Runtime.Remoting.Messaging;//調用AsyncResult類庫
using System.Threading;
delegate long MyDel(int first, int second);
class Program
{
    static long Sum(int x, int y)
    {
        Console.WriteLine("              Inside Sum");
        Thread.Sleep(100);
        return x + y;
    }
    static void CallWhenDone(IAsyncResult iar)
    {
        Console.WriteLine("              Inside CallWhenDone.");
        AsyncResult ar = (AsyncResult) iar;
        MyDel del = (MyDel)ar.AsyncDelegate;
        long result = del.EndInvoke(iar);
        Console.WriteLine("              The result is: {0}.",result);
    }
    static void Main()
    {
        MyDel del = new MyDel(Sum);
        Console.WriteLine("Before BeginInvoke");
        IAsyncResult iar =del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null);
        Console.WriteLine("Doing more work in Main.");
        Thread.Sleep(500);
        Console.WriteLine("Done with Main. Exiting.");
        Console.ReadKey();
    }
}

回調(callback)模式的輸出:

Before BeginInvoke
Doing more work in Main.
              Inside Sum
              Inside CallWhenDone.
              The result is: 8.
Done with Main. Exiting.

計時器

計時器提供了另一種按期地重複運行異步方法的方式。儘管在.NET BCL中有好幾個可用的Timer類,但在這裏咱們只會介紹System.Threading命名空間中的那個。
有關計時器類須要瞭解的重要事項以下。

  • 計時器在每次時間到期以後調用回調方法。回調方法必須是TimerCallback委託形式的,結構以下所示。它接受一個object類型做爲參數,而且返回類型是void
    void TimerCallback( object state )
  • 當計時器到期以後,系統會從線程池中的線程上開啓一個回調方法,提供state對象做爲其參數,而且開始運行
  • 咱們能夠設置的計時器的一些特性以下
    • dueTime是回調方法首次被調用以前的時間。若是dueTime被設爲特殊的值Timeout.Infinite,則計時器不會開始。若是被設置爲0,回調函數會被當即調用
    • period是兩次成功調用回調函數之間的時間間隔。若是它的值設置爲Timeout.Infinite,回調在首次被調用以後不會再被調用
    • state能夠是null或在每次回調方法執行時要傳入的對象的引用

Timer類的構造函數接受回調方法名稱、dueTimeperiod以及state做爲參數。Timer有不少構造函數,最爲經常使用的形式以下:
Timer(TimerCallback callback,object state,uint dueTime,uint period)
例:建立Timer對象的示例:

                              回調的              在2000毫秒後
                               名字                第一次調用
                                ↓                     ↓
Timer myTimer = new Timer ( MyCallback, someObject, 2000, 1000 );
                                             ↑              ↑
                                         傳給回調的      每1000毫秒
                                           對象          調用一次

一旦Timer對象被建立,咱們可使用Change方法來改變它的dueTimeperiod方法。
以下代碼給出了一個使用計時器的示例。Main方法建立一個計時器,2秒鐘以後它會首次調用回調,而後每隔1秒調用1次。回調方法只是輸出了包含它被調用的次數的消息。

using System;
using System.Threading;
namespace Timers
{
    class Program
    {
        int TimesCalled = 0;
        void Display(object state)
        {
            Console.WriteLine("{0} {1}", (string)state, ++TimesCalled);
        }
        static void Main()
        {
            Program p = new Program();
            Timer myTimer = new Timer     //2s後第一次調用,每1s重複依次
                (p.Display, "Processing timer event", 2000, 1000);
            Console.WriteLine("Timer started.");
            Console.ReadLine();
        }
    }
}

輸出:

Timer started.
Processing timer event 1
Processing timer event 2
Processing timer event 3
Processing timer event 4
Processing timer event 5

.NET BCL還提供了幾個其餘計時器類,每個都有其用途。其餘計時器類以下所示。

  • System.Windows.Forms.Timer 這個類在Windows應用程序中使用,用來按期把WM_TIMER消息放到程序的消息隊列中。當程序從隊列獲取消息後,它會在主用戶接口線程中同步處理,這對Windows應用程序來講很是重要
  • System.Timers.Timer 這個類更復雜,它包含了不少成員,使咱們能夠經過屬性和方法來操做計時器。它還有一個叫作Elapsed的成員事件,每次時間到期就會發起這個事件。這個計時器能夠運行在用戶接口線程或工做者線程上
相關文章
相關標籤/搜索