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的成员事件,每次时间到期就会发起这个事件。这个计时器能够运行在用户接口线程或工做者线程上
相关文章
相关标签/搜索