【温故而知新-万花筒】C# 异步编程 逆变 协变 委托 事件 事件参数 迭代 线程、多线程、线程池、后台线程

额基本脱离了2.0 3.5的时代了。在.net 4.0+ 时代。一切都是辣么简单!

参考文档:

http://www.cnblogs.com/linzheng/archive/2012/04/11/2442061.html

http://www.cnblogs.com/pugang/archive/2011/11/09/2242380.html

http://www.cnblogs.com/LoveJenny/archive/2012/03/13/2392747.html

http://www.cnblogs.com/luqixinhe/archive/2013/07/18/3197645.html

http://www.cnblogs.com/yank/p/3239767.html

http://www.cnblogs.com/apsnet/archive/2012/07/08/2581475.html

http://www.cnblogs.com/heyuquan/archive/2013/04/18/Task-based-Asynchronous-Pattern.html

msdn 解释如下:

“协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型。

“逆变”则是指能够使用派生程度更小的类型。

解释的很正确,大致就是这样,不过不够直白。

直白的理解:

“协变”->”和谐的变”->”很自然的变化”->string->object :协变。

“逆变”->”逆常的变”->”不正常的变化”->object->string 逆变。

上面是个人对协变和逆变的理解,比起记住那些派生,类型,原始指定,更大,更小之类的词语,个人认为要容易点。

下面是一则笑话:

一个星期的每一天应该这样念:

星期一 = 忙day;
星期二 = 求死day;
星期三 = 未死day;
星期四 = 受死day;
星期五 = 福来day;
星期六 = 洒脱day;
星期天 = 伤day

为了演示协变和逆变,以及之间的区别,请创建控制台程序CAStudy,手动添加两个类:

因为是演示,所以都是个空类,

只是有一点记住Dog 继承自Animal,

所以Dog变成Animal 就是和谐的变化(协变),而如果Animal 变成Dog就是不正常的变化(逆变)

在Main函数中输入:

因为Dog继承自Animal,所以Animal aAnimal = aDog; aDog 会隐式的转变为Animal.

但是List<Dog> 不继承List<Animal> 所以出现下面的提示:

如果想要转换的话,应该使用下面的代码:

List<Animal> lstAnimal2 = lstDogs.Select(d => (Animal)d).ToList();

可以看到一个lstDogs 变成lstAnimal 是多么复杂的操作了。

正因如此,所以微软新增了两个关键字:Out,In,下面是他们的msdn解释:

协变的英文是:“covariant”,逆变的英文是:“Contravariant”

为什么Microsoft选择的是”Out” 和”In” 作为特性而不是它们呢?

我个人的理解:

因为协变和逆变的英文太复杂了,并没有体现协变和逆变的不同,但是out 和 in 却很直白。

out: 输出(作为结果),in:输入(作为参数)

所以如果有一个泛型参数标记为out,则代表它是用来输出的,只能作为结果返回,而如果有一个泛型参数标记为in,则代表它是用来输入的,也就是它只能作为参数。

目前out 和in 关键字只能在接口和委托中使用,微软使用out 和 in 标记的接口和委托大致如下:

先看下第一个IEnumerable<T>

和刚开始说的一样,T 用out 标记,所以T代表了输出,也就是只能作为结果返回。

public static void Main()

{

Dog aDog = new Dog();

Animal aAnimal = aDog;

List<Dog> lstDogs = new List<Dog>();

//List<Animal> lstAnimal = lstDogs;

List<Animal> lstAnimal2 = lstDogs.Select(d => (Animal)d).ToList();

IEnumerable<Dog> someDogs = new List<Dog>();

IEnumerable<Animal> someAnimals = someDogs;

}

因为T只能做结果返回,所以T不会被修改, 编译器就可以推断下面的语句强制转换合法,所以

IEnumerable<Animal> someAnimals = someDogs;

可以通过编译器的检查,反编译代码如下:

虽然通过了C#编译器的检查,但是il 并不知道协变和逆变,还是得乖乖的强制转换。

在这里我看到了这句话:

IEnumerable<Animal> enumerable2 = (IEnumerable<Animal>) enumerable1;

那么是不是可以List<Animal> lstAnimal3 = (List<Animal>)lstDogs; 呢?

想要回答这个问题需要在回头看看Clr via C# 关于泛型和接口的章节了,我就不解释了,

答案是不可以。

上面演示的是协变,接下来要演示下逆变。

为了演示逆变,那么就要找个in标记的接口或者委托了,最简单的就是:

在Main函数中添加:

Action<Animal> actionAnimal = new Action<Animal>(a => {/*让动物叫*/ });

Action<Dog> actionDog = actionAnimal;

actionDog(aDog);

很明显actionAnimal 是让动物叫,因为Dog是Animal,那么既然Animal 都能叫,Dog肯定也能叫。

In 关键字:逆变,代表输入,代表着只能被使用,不能作为返回值,所以C#编译器可以根据in关键字推断这个泛型类型只能被使用,所以Action<Dog> actionDog = actionAnimal;可以通过编译器的检查。

再次演示Out关键字:

添加两个类:

public interface IMyList<out T>

{

T GetElement();

}

public class MyList<T> : IMyList<T>

{

public T GetElement()

{

return default(T);

}

}

因为out 关键字,所以下面的代码可以通过编译

IMyList<Dog> myDogs = new MyList<Dog>();

IMyList<Animal> myAnimals = myDogs;

将上面的两个类修改为:

public interface IMyList<out T>

{

T GetElement();

void ChangeT(T t);

}

public class MyList<T> : IMyList<T>

{

public T GetElement()

{

return default(T);

}

public void ChangeT(T t)

{

//Change T

}

}

编译:

因为T被out修饰,所以T只能作为参数。

同样修改两个类如下:

public interface IMyList<in T>

{

T GetElement();

void ChangeT(T t);

}

public class MyList<T> : IMyList<T>

{

public T GetElement()

{

return default(T);

}

public void ChangeT(T t)

{

//Change T

}

}

这一次使用in关键字。

编译:

因为用in关键字标记,所以T只能被使用,不能作为返回值。

最后修改代码为:

public interface IMyList<in T>

{

void ChangeT(T t);

}

public class MyList<T> : IMyList<T>

{

public void ChangeT(T t)

{

//Change T

}

}

编译成功,因为in代表了逆变,所以

IMyList<Animal> myAnimals = new MyList<Animal>();

IMyList<Dog> myDogs = myAnimals;

可以编译成功!。

看到过园子里面几篇协变和逆变的文章,但是总觉得写得不够清晰,文章这东西注重要是要把自己想表达的观点表达出来,这个过程应该是把复杂的东西消化出来从而简单化,清晰化,而不是故弄玄虚,反其道而行之,下面我们言归正传啦。

我们先来看一段MSDN原文给协变,逆变和变体下个定义:

A generic interface or delegate is called variant if its generic parameters are declared covariant or contravariant. Both C# and Visual Basic enable you to create your own variant interfaces and delegates.

如果泛型接口或委托的泛型参数声明为协变或逆变,则将该泛型接口或委托称为“变体”。 C# 和 Visual Basic 都允许您创建自己的变体接口和委托。

通俗解释:

变体定义:带有协变或逆变参数的泛型接口或委托。也就是说协变和逆变主要关注点在泛型接口或委托。

那什么又是协变和逆变呢?

我们先来看下面一个来自MSDN的例子:

 
1 // 协变
2 IEnumerable<string>strings = new List<string>();
3 IEnumerable<object> objects = strings;
 
大家看到了么一个声明为IEnumerable<string>的 接口类型被赋给了一个更低 级别的IEnumerable<object>.
对,这就是协变。再来看一个例子:
 
 
class Base
{
    public static void PrintBases(IEnumerable<Base> bases)
    {
        foreach(Base b in bases)
        {
            Console.WriteLine(b);
        }
    }
}
 
class Derived : Base
{
    public static void Main()
    {
        List<Derived> dlist = new List<Derived>();
 
        Derived.PrintBases(dlist);//由于IEnumerable<T>接口是协变的,所以PrintBases(IEnumerable<Base> bases)
                                  //可以接收一个更加具体化的IEnumerable<Derived>作为其参数。
        IEnumerable<Base> bIEnum = dlist;
    }
}
 
 

下面给协变下个定义:

协变:让一个带有协变参数的泛型接口(或委托)可以接收类型更加精细化,具体化的泛型接口(或委托)作为参数,可以看成OO中多态的一个延伸。

 
// 逆变
// Assume that the following method is in the class: 
// static void SetObject(object o) { } 
Action<object> actObject = SetObject;
Action<string> actString = actObject; 
//委托actString中以后要使用更加精细化的类型string不能再使用object啦!
string strHello(“Hello”); 
actString(strHello);
 

大家看到了么?一个声明为Action<object>的类型被赋给了一个Action<string>,大家都知道,Action<T>接收参数,没有返回值,所以其中的object和string是其参数,这个过程其实就是参数的约束更加强了,也就是说让参数类型更加精细化。下面我们来给逆变下个定义:

逆变:让一个带有协变参数的泛型接口(或委托)可以接收粒度更粗的泛型接口或委托作为参数,这个过程实际上是参数类型更加精细化的过程。

一句话总结:协变让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);逆变让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。

通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。 对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。

异步编程:.NET 4.5 基于任务的异步编程模型(TAP)

传送门:异步编程系列目录……

最近我为大家陆续介绍了“IAsyncResult异步编程模型 (APM)”和“基于事件的异步编程模式(EAP)”两种异步编程模型。在.NET4.0 中Microsoft又为我们引入了新的异步编程模型“基于任务的异步编程模型(TAP)”,并且推荐我们在开发新的多线程应用程序中首选TAP,在.NET4.5中更是对TPL库进行了大量的优化与改进。那现在我先介绍下TAP具有哪些优势:

  1. 目前版本(.NET4.X)的任务调度器(TaskScheduler)依赖于底层的线程池引擎。通过局部队列的任务内联化(task inlining)和工作窃取机制可以为我们提升程序性能。
  2. 轻松实现任务等待、任务取消、延续任务、异常处理(System.AggregateException)、GUI线程操作。
  3. 在任务启动后,可以随时以任务延续的形式注册回调。
  4. 充分利用现有的线程,避免创建不必要的额外线程。
  5. 结合C#5.0引入async和await关键字轻松实现“异步方法”。

示例源码:异步编程:.NET 4.5 基于任务的异步编程模型(TAP).rar

术语:

APM              异步编程模型,Asynchronous Programming Model

EAP               基于事件的异步编程模式,Event-based Asynchronous Pattern

TAP               基于任务的异步编程模式,Task-based Asynchronous Pattern

TPL                任务并行库,Task Parallel Library

理解CLR线程池引擎、理解全局队列、理解线程的局部队列及性能优势

  1. CLR线程池引擎

CLR线程池引擎维护了一定数量的空闲工作线程以支持工作项的执行,并且能够重用已有的线程以避免创建新的不必要的线程所花费的昂贵的处理过程。并且使用爬山算法(hill-climbing algorithm)检测吞吐量,判断是否能够通过更多的线程来完成更多的工作项。这个算法的判断依据是工作项所需某些类型资源的可用情况,例如:CPU、网络带宽或其他。此外这个算法还会考虑一个饱和点,即达到饱和点的时候,创建更多地线程反而会降低吞吐量。(线程池的详细介绍请看《异步编程:使用线程池管理线程》

目前版本的TAP的任务调度器(TaskScheduler)基于CLR线程池引擎实现。当任务调度器(TaskScheduler开始分派任务时:

1)        在主线程或其他并没有分配给某个特定任务的线程的上下文中创建并启动的任务,这些任务将会在全局队列中竞争工作线程。这些任务被称为顶层任务

2)        然而,如果是在其他任务的上下文中创建的任务(子任务或嵌套任务),这些任务将被分配在线程的局部队列中。

嵌套任务:

是在另一个任务的用户委托中创建并启动的任务。

子任务:

是使用TaskCreationOptions.AttachedToParent选项创建顶层任务的嵌套任务或延续任务;或使用TaskContinuationOptions.AttachedToParent选项创建的延续任务的嵌套任务或延续任务。(应用程序使用TaskCreationOptions.DenyChildAttach选项创建父任务。此选项指示运行时会取消子任务的AttachedToParent规范)

如果你不想特定的任务放入线程的局部队列,那么可以指定TaskCreationOptions.PreferFairness或TaskContinuationOptions.PreferFairness枚举参数。(使Task与ThreadPool.QueueUserWorkItem行为相同)

  1. 线程池的全局队列

当调用ThreadPool.QueueUserWorkItem()添加工作项时,该工作项会被添加到线程池的全局队列中。线程池中的空闲线程以FIFO的顺序将工作项从全局队列中取出并执行,但并不能保证按某个指定的顺序完成。

线程的全局队列是共享资源,所以内部会实现一个锁机制。当一个任务内部会创建很多子任务时,并且这些子任务完成得非常快,就会造成频繁的进入全局队列和移出全局队列,从而降低应用程序的性能。基于此原因,线程池引擎为每个线程引入了局部队列。

  1. 线程的局部队列为我们带来两个性能优势:任务内联化(task inlining)和工作窃取机制。

1)        任务内联化(task inlining)----活用顶层任务工作线程

我们用一个示例来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static void Main(string[] args)
{
    Task headTask= new Task(() =&gt;
    {
        DoSomeWork(null);
    });
    headTask.Start();
    Console.Read();
}
private static void DoSomeWork(object obj)
{
    Console.WriteLine("任务headTask运行在线程“{0}”上",
        Thread.CurrentThread.ManagedThreadId);
 
    var taskTop = new Task(() =&gt;
    {
        Thread.Sleep(500);
        Console.WriteLine("任务taskTop运行在线程“{0}”上",
            Thread.CurrentThread.ManagedThreadId);
    });
    var taskCenter = new Task(() =&gt;
    {
        Thread.Sleep(500);
        Console.WriteLine("任务taskCenter运行在线程“{0}”上",
            Thread.CurrentThread.ManagedThreadId);
    });
    var taskBottom = new Task(() =&gt;
    {
        Thread.Sleep(500);
        Console.WriteLine("任务taskBottom运行在线程“{0}”上",
            Thread.CurrentThread.ManagedThreadId);
    });
    taskTop.Start();
    taskCenter.Start();
    taskBottom.Start();
    Task.WaitAll(new Task[] { taskTop, taskCenter, taskBottom });
}

结果:

分析:(目前内联机制只有出现在等待任务场景)

这个示例,我们从Main方法主线程中创建了一个headTask顶层任务并开启。在headTask任务中又创建了三个嵌套任务并最后WaitAll() 这三个嵌套任务执行完成(嵌套任务安排在局部队列)。此时出现的情况就是headTask任务的线程被阻塞,而“任务内联化”技术会使用阻塞的headTask的线程去执行局部队列中的任务。因为减少了对额外线程需求,从而提升了程序性能。

局部队列“通常”以LIFO的顺序抽取任务并执行,而不是像全局队列那样使用FIFO顺序。LIFO顺序通常用有利于数据局部性,能够在牺牲一些公平性的情况下提升性能。

数据局部性的意思是:运行最后一个到达的任务所需的数据都还在任何一个级别的CPU高速缓存中可用。由于数据在高速缓存中任然是“热的”,因此立即执行最后一个任务可能会获得性能提升。

2)        工作窃取机制----活用空闲工作线程

当一个工作线程的局部队列中有很多工作项正在等待时,而存在一些线程却保持空闲,这样会导致CPU资源的浪费。此时任务调度器(TaskScheduler)会让空闲的工作线程进入忙碌线程的局部队列中窃取一个等待的任务,并且执行这个任务。

由于局部队列为我们带来了性能提升,所以,我们应尽可能地使用TPL提供的服务(任务调度器(TaskScheduler)),而不是直接使用ThreadPool的方法。

任务并行Task

一个任务表示一个异步操作。任务运行的时候需要使用线程,但并不是说任务取代了线程,理解这点很重要。事实上,在《异步编程:.NET4.X 数据并行》中介绍的System.Threading.Tasks.Parallel类构造的并行逻辑内部都会创建Task,而它们的并行和并发执行都是由底层线程支持的。任务和线程之间也没有一对一的限制关系,通用语言运行时(CLR)会创建必要的线程来支持任务执行的需求。

  1. Task简单的实例成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable
{
    public Task(Action<object> action, object state
          , CancellationToken cancellationToken,TaskCreationOptions creationOptions);
 
    // 获取此 Task 实例的唯一 ID。
    public int Id { get; }
    // 获取用于创建此任务的TaskCreationOptions。
    public TaskCreationOptions CreationOptions { get; }
    // 获取此任务的TaskStatus。
    public TaskStatus Status { get; }
    // 获取此 Task 实例是否由于被取消的原因而已完成执行。
    public bool IsCanceled { get; }
    // 获取 Task 是否由于未经处理异常的原因而完成。
    public bool IsFaulted { get; }
    // 获取导致 Task 提前结束的System.AggregateException。
    public AggregateException Exception { get; }
 
    #region IAsyncResult接口成员
    private bool IAsyncResult.CompletedSynchronously { get;}
    private WaitHandleIAsyncResult.AsyncWaitHandle { get; }
 
    // 获取在创建 Task 时提供的状态对象,如果未提供,则为 null。
    public object AsyncState { get; }
    // 获取此 Task 是否已完成。
    public bool IsCompleted { get; }
    #endregion
 
    // 释放由 Task 类的当前实例占用的所有资源。
    public void Dispose();
    ……
}

分析:

1)        CancellationToken、IsCancel

对于长时间运行的计算限制操作来说,支持取消是一件很“棒”的事情。.NET 4.0提供了一个标准的取消操作模式。即通过使用CancellationTokenSource创建一个或多个取消标记CancellationToken(cancellationToken可在线程池中线程或 Task 对象之间实现协作取消),然后将此取消标记传递给应接收取消通知的任意数量的线程或Task对象。当调用CancellationToken关联的CancellationTokenSource对象的Cancle()时,每个取消标记(CancellationToken)上的IsCancellationRequested属性将返回true。异步操作中可以通过检查此属性做出任何适当响应。也可调用取消标记的ThrowIfCancellationRequested()方法来抛出OperationCanceledException异常。

更多关于CancellationToken与CancellationTokenSource的介绍及示例请看《协作式取消》….

       Task任务中实现取消,可以使用以下几种选项之一终止操作:

  1. i.              简单地从委托中返回。在许多情况下,这样已足够;但是,采用这种方式“取消”的任务实例会转换为RanToCompletion状态,而不是 Canceled 状态。
  2. ii.              创建Task时传入CancellationToken标识参数,并调用关联CancellationTokenSource对象的Cancel()方法:

a)        如果Task还未开始,那么Task实例直接转为Canceled状态。(注意,因为已经Canceled状态了,所以不能再在后面调用Start())

b)        (见示例:TaskOperations.Test_Cancel();)如果Task已经开始,在Task内部必须抛出OperationCanceledException异常(注意,只能存在OperationCanceledException异常,可优先考虑使用CancellationToken的ThrowIfCancellationRequested()方法),Task实例转为Canceled状态。

若对抛出OperationCanceledException异常且状态为Canceled的Task进行等待操作(如:Wait/WaitAll),则会在Catch块中捕获到OperationCanceledException异常,但是此异常指示Task成功取消,而不是有错误的情况。因此IsCancel为true;IsFaulted为false且Exception属性为null。

  1. iii.              对于使用TaskContinuationOptions枚举值为NotOn或OnlyOn创建的延续任务A,在其前面的任务结束状态不匹配时,延续任务A将转换为Canceled状态,并且不会运行。

2)        TaskCreationOptions枚举

定义任务创建、调度和执行的一些可选行为。

None

指定应使用默认行为。

PreferFairness

较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。(Prefer:更喜欢 ; Fair:公平的)

LongRunning

该任务需要很长时间运行,因此,调度器可以对这个任务使用粗粒度的操作(默认TaskScheduler为任务创建一个专用线程,而不是排队让一个线程池线程来处理,可通过在延续任务中访问:Thread.CurrentThread.IsThreadPoolThread属性判别)。比如:如果任务可能需要好几秒的时间运行,那么就使用这个参数。相反,如果任务只需要不到1秒钟的时间运行,那么就不应该使用这个参数。

AttachedToParent

指定此枚举值的Task,其内部创建的Task或通过ContinueWith()创建的延续任务都为子任务。(父级是顶层任务)

DenyChildAttach

如果尝试附加子任务到创建的任务,指定System.InvalidOperationException将被引发。

HideScheduler

创建任务的执行操作将被视为TaskScheduler.Default默认计划程序。

3)        IsCompleted

Task实现了IAsyncResult接口。在任务处于以下三个最终状态之一时IsCompleted返回 true:RanToCompletion、 Faulted 或 Canceled。

4)        TaskStatus枚举

表示 Task 的生命周期中的当前阶段。一个Task实例只会完成其生命周期一次,即当Task到达它的三种可能的最终状态之一时,Task就结束并释放。

可能的初始状态

Created

该任务已初始化,但尚未被计划。

WaitingForActivation

只有在其它依赖的任务完成之后才会得到调度的任务的初始状态。这种任务是使用定义延续的方法创建的。

WaitingToRun

该任务已被计划执行,但尚未开始执行。

中间状态

Running

该任务正在运行,但尚未完成。

WaitingForChildrenToComplete

该任务已完成执行,正在隐式等待附加的子任务完成。

可能的最终状态

RanToCompletion

已成功完成执行的任务。

Canceled

该任务已通过对其自身的CancellationToken引发OperationCanceledException异常

Faulted

由于未处理异常的原因而完成的任务。

状态图如下:

5)        Dispose()

尽管Task为我们实现了IDisposable接口,但依然不推荐你主动调用Dispose()方法,而是由系统终结器进行清理。原因:

a)        Task调用Dispose()主要释放的资源是WaitHandle对象。

b)        .NET4.5 对.NET4.0 中提出的Task进行过大量的优化,让其尽量不再依赖WaitHandle对象(eg:.NET4.0种Task的WaitAll()/WaitAny()的实现依赖于WaitHandle)。

c)        在使用Task时,大多数情况下找不到一个好的释放点,保证该Task已经完成并且没有被其他地方在使用。

d)        Task.Dispose()方法在“.NET Metro风格应用程序”框架所引用的程序集中甚至并不存在(即此框架中Task没有实现IDisposable接口)。

更详细更专业的Dispose()讨论请看《.NET4.X并行任务Task需要释放吗?》

  1. Task的实例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 获取用于等待此 Task 的等待者。
public TaskAwaiter GetAwaiter();
// 配置用于等待此System.Threading.Tasks.Task的awaiter。
// 参数:continueOnCapturedContext:
//     试图在await返回时夺取原始上下文,则为 true;否则为 false。
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
 
// 对提供的TaskScheduler同步运行 Task。
public void RunSynchronously(TaskScheduler scheduler);
// 启动 Task,并将它安排到指定的TaskScheduler中执行。
public void Start(TaskScheduler scheduler);
// 等待 Task 完成执行过程。
public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken);
 
// 创建一个在目标 Task 完成时执行的延续任务。
public Task ContinueWith(Action<Task, object> continuationAction, object state
        , CancellationToken cancellationToken
        , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
public Task<TResult>ContinueWith<TResult>(
        Func<Task, object, TResult> continuationFunction
        , object state,CancellationToken cancellationToken
        , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
……

分析:

1)        TaskContinuationOptions

在创建一个Task作为另一个Task的延续时,你可以指定一个TaskContinuationOptions参数,这个参数可以控制延续另一个任务的任务调度和执行的可选行为。

None

默认情况下,完成前面的任务之后“都”将安排运行延续任务,而不考虑前面任务的最终TaskStatus。

AttachedToParent

对延续任务指定此枚举值,表示该延续任务内部创建的新Task或通过ContinueWith()创建的延续任务都为子任务。(父级是延续任务)

PreferFairness

LongRunning

DenyChildAttach

HideScheduler

参考:TaskCreationOptions枚举

LazyCancellation

在延续取消的情况下,防止延续的完成直到完成先前的任务。

NotOnRanToCompletion

NotOnFaulted

NotOnCanceled

指定不应在延续任务前面的任务“已完成运行、引发了未处理异常、已取消”的情况下安排延续任务。

此选项对多任务延续无效。

OnlyOnCanceled

OnlyOnFaulted

OnlyOnRanToCompletion

指定只应在延续任务前面的任务“已取消、引发了未处理异常、已完成运行”的情况下才安排延续任务。

ExecuteSynchronously

指定应同步执行延续任务。指定此选项后,延续任务将在导致前面的任务转换为其最终状态的相同线程上运行。

注意:

a)        如果使用默认选项TaskContinuationOptions.None,并且之前的任务被取消了,那么延续任务任然会被调度并启动执行。

b)        如果该条件在前面的任务准备调用延续时未得到满足,则延续将直接转换为 Canceled 状态,之后将无法启动。

c)        如果调用多任务延续(即:调用TaskFactory或TaskFactory<TResult>的静态ContinueWhenAll和ContinueWhenAny方法)时,NotOn和OnlyOn六个标识或标识的组合都是无效的。也就是说,无论先驱任务是如何完成的,ContinueWhenAll和ContinueWhenAny都会执行延续任务。

d)        TaskContinuationOptions.ExecuteSynchronously,指定同步执行延续任务。延续任务会使用前一个任务的数据,而保持在相同线程上执行就能快速访问高速缓存中的数据,从而提升性能。此外,也可避免调度这个延续任务产生不必要的额外线程开销。

如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。只应同步执行运行时间非常短的延续任务。

2)        开启任务

只有Task处于TaskStatus.Created状态时才能使用实例方法Start()。并且,只有在使用Task的公共构造函数构造的Task实例才能处于TaskStatus.Created状态。

当然我们还知道有其他方式可以创建Task并开启任务,比如Task.Run()/Task.ContinueWith()/Task.Factory.StartNew()/TaskCompletionSource/异步方法(即使用async与await关键字的方法),但是这些方法返回的Task已经处于开启状态,即不能再调用Start()。更丰富更专业的讨论请看《.NET4.X 并行任务中Task.Start()的FAQ》

3)        延续任务ContinueWith

a)        ContinueWith() 方法可创建一个根据TaskContinuationOptions参数限制的延续任务。可以为同一个Task定义多个延续任务让它们并行执行。

比如,为t1定义两个并行延续任务t2、t3.

1
2
3
Task<int> t1 = new Task<int>(() => { return 1; });
Task<int> t2 = t1.ContinueWith<int>(Work1,……);
Task<int> t3 = t1.ContinueWith<int>(Work1,……);

b)        调用Wait()方法和Result属性会导致线程阻塞,极有可能造成线程池创建一个新线程,这增大了资源的消耗,并损害了伸缩性。可以在延续任务中访问这些成员,并做相应操作。

c)        对前面任务的引用将以参数形式传递给延续任务的用户委托,以将前面任务的数据传递到延续任务中。

4)        Wait()

一个线程调用Wait()方法时,系统会检查线程要等待的Task是否已开始执行。

a)        如果是,调用Wait()的线程会阻塞,直到Task运行结束为止。

b)        如果Task还没有开始执行,系统可能(取决于局部队列的内联机制)使用调用Wait()的线程来执行Task。如果发生这种情况,那么调用Wait()的线程不会阻塞;它会执行Task并立刻返回。

  1. i.              这样做的好处在于,没有线程会被阻塞,所以减少了资源的使用(因为不需要创建一个线程来替代被阻塞的线程),并提升了性能(因为不需要花时间创建一个线程,也没有上下文切换)。
  2. ii.              但不好的地方在于,假如线程在调用Wait()前已经获得一个不可重入的线程同步锁(eg:SpinLock),而Task试图获取同一个锁,就会造成一个死锁的线程!

5)        RunSynchronously

可在指定的TaskScheduler或TaskScheduler.Current中同步运行 Task。即RunSynchronously()之后的代码会阻塞到Task委托执行完毕。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
Task task1 = new Task(() =>
{
    Thread.Sleep(5000);
    Console.WriteLine("task1执行完毕。");
});
task1.RunSynchronously();
Console.WriteLine("执行RunSynchronously()之后的代码。");
 
// 输出==============================
// task1执行完毕。
// 执行RunSynchronously()之后的代码。
  1. Task的静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 返回当前正在执行的 Task 的唯一 ID。
public static int? CurrentId{ get; }
// 提供对用于创建 Task 和 Task<TResult>实例的工厂方法的访问。
public static TaskFactory Factory { get; }
// 创建指定结果的、成功完成的Task<TResult>。
public static Task<TResult> FromResult<TResult>(TResult result);
 
// 创建将在指定延迟后完成的任务。
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken);
 
// 将在线程池上运行的指定工作排队,并返回该工作的任务句柄。
public static Task Run(Action action, CancellationToken cancellationToken);
// 将在线程池上运行的指定工作排队,并返回该工作的 Task(TResult) 句柄。
public static Task<TResult>  Run<TResult>(Func<TResult> function, CancellationToken  cancellationToken);
// 将在线程池上运行的指定工作排队,并返回 function 返回的任务的代理项。
public static Task Run(Func<Task> function, CancellationToken cancellationToken);
// 将在线程池上运行的指定工作排队,并返回 function 返回的 Task(TResult) 的代理项。
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);
 
// 等待提供的所有 Task 对象完成执行过程。
public static bool WaitAll(Task[] tasks, intmillisecondsTimeout, CancellationToken cancellationToken);
// 等待提供的任何一个 Task 对象完成执行过程。
// 返回结果:
//     已完成的任务在 tasks 数组参数中的索引,如果发生超时,则为 -1。
public static int WaitAny(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);
 
// 所有提供的任务已完成时,创建将完成的任务。
public static Task WhenAll(IEnumerable<Task> tasks);
public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);
// 任何一个提供的任务已完成时,创建将完成的任务。
public static Task<Task> WhenAny(IEnumerable<Task> tasks);
public static Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks);
 
// 创建awaitable,等待时,它异步产生当前上下文。
// 返回结果:等待时,上下文将异步转换回等待时的当前上下文。
// 如果当前SynchronizationContext不为 null,则将其视为当前上下文。
// 否则,与当前执行任务关联的任务计划程序将视为当前上下文。
public static YieldAwaitable Yield();

分析:

1)        FromResult<TResult>(TResult result);

创建指定结果的、成功完成的Task<TResult>。我们可以使用此方法创建包含预先计算结果/缓存结果的 Task<TResult>对象,示例代码或CachedDownloads.cs示例文件。

2)        Delay

创建将在指定延迟后完成的任务,返回Task。可以通过await或Task.Wait()来达到Thread.Sleep()的效果。尽管,Task.Delay() 比Thread.Sleep()消耗更多的资源,但是Task.Delay()可用于为方法返回Task类型;或者根据CancellationToken取消标记动态取消等待。

Task.Delay()等待完成返回的Task状态为RanToCompletion;若被取消,返回的Task状态为Canceled。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
Task.Factory.StartNew(() => { Thread.Sleep(1000); tokenSource.Cancel(); });
Console.WriteLine("Begin taskDelay1");
Task taskDelay1 = Task.Delay(100000, token);
try
{
    taskDelay1.Wait();
}
catch (AggregateException ae)
{
    foreach (var v in ae.InnerExceptions)
        Console.WriteLine(ae.Message + " " + v.Message);
}
taskDelay1.ContinueWith((t) =>Console.WriteLine(t.Status.ToString()));
 
Thread.Sleep(100);
Console.WriteLine();
 
Console.WriteLine("Begin taskDelay2");
Task taskDelay2 = Task.Delay(1000);
taskDelay2.ContinueWith((t) =>Console.WriteLine(t.Status.ToString()));
// 输出======================================
// Begin taskDelay1
// 发生一个或多个错误。已取消一个任务。
// Canceled
//
// Begin taskDelay2
// Completed
  1. Task<TResult>:Task

Task<TResult>继承自Task,表示一个可以返回值的异步操作,提供Result只读属性用于访问异步操作的返回值。该属性会阻塞线程,直到Task执行完毕并返回值。

System.Threading.Tasks.TaskFactory

  1. 设置共用\默认的参数

通过TaskFactory对象提供的Scheduler、CancellationToken、CreationOption和ContinuationOptions属性可以为Task设置共用\默认的参数,以便快捷的创建Task或延续任务。影响StartNew()、ContinueWhenAll()|ContinueWhenAny()、FromAsync()方法的默认参数设置。

  1. StartNew()

Task.Factory.StartNew()可快速创建一个Task并且开启任务。代码如下:

1
var t = Task.Factory.StartNew(someDelegate);

这等效于:

1
2
var t = new Task(someDelegate);
t.Start();

表现方面,前者更高效。Start()采用同步方式运行以确保任务对象保持一致的状态即使是同时调用多次Start(),也可能只有一个调用会成功。相比之下,StartNew()知道没有其他代码能同时启动任务,因为在StartNew()返回之前它不会将创建的Task引用给任何人,所以StartNew()不需要采用同步方式执行。更丰富更专业的讨论请看《.NET4.X 并行任务中Task.Start()的FAQ》

  1. ContinueWhenAll()
1
2
3
public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction
    , CancellationToken cancellationToken
    , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);

创建一个延续 Task 或延续 Task<TResult>,它将在提供的一组任务完成后马上开始。延续任务操作委托接受一个Task[]数组做参数。

  1. ContinueWhenAny()
1
2
3
public Task ContinueWhenAny(Task[] tasks, Action<Task> continuationAction
    , CancellationToken cancellationToken
    , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);

创建一个延续 Task 或延续 Task<TResult>,它将在提供的组中的任何一个任务完成后马上开始。延续任务操作委托接受一个 Task 做参数。

  1. 通过Task.TaskFactory.FromAsync() 实例方法,我们可以将APM转化为TAP。示例见此文的后面小节AMP转化为TAP和EAP转化为TAP

System.Threading.Tasks.TaskScheduler

TaskScheduler表示一个处理将任务排队到线程中的底层工作对象。TaskScheduler通常有哪些应用呢?

  1. TaskScheduler是抽象类,可以继承它实现自己的任务调度计划。如:默认调度程序ThreadPoolTaskScheduler、与SynchronizationContext.Current关联的SynchronizationContextTaskScheduler。
  2. 由TaskScheduler.Default获取默认调度程序ThreadPoolTaskScheduler。
  3. 由TaskScheduler.Current获取当前任务执行的TaskScheduler。
  4. 由 TaskScheduler.TaskSchedulerFromCurrentSynchronizationContext() 方法获取与SynchronizationContext.Current关联的SynchronizationContextTaskScheduler,SynchronizationContextTaskScheduler上的任务都会通过SynchronizationContext.Post()在同步上下文中进行调度。通常用于实现跨线程更新控件。
  5. 通过MaximumConcurrencyLevel设置任务调度计划能支持的最大并发级别。
  6. 通过UnobservedTaskException事件捕获未被观察到的异常。

System.Threading.Tasks.TaskExtensions

提供一组用于处理特定类型的 Task 实例的静态方法。将特定Task实例进行解包操作。

1
2
3
4
5
public static class TaskExtensions
{
    public static Task<TResult> Unwrap<TResult>(this Task<Task<TResult>> task);
    public static Task Unwrap(this Task<Task> task);
}

AMP转化为TAP和EAP转化为TAP

  1. AMP转化为TAP

通过Task.TaskFactory.FromAsync() 实例方法,我们可以将APM转化为TAP。

注意点:

1)        FromAsync方法返回的任务具有WaitingForActivation状态,并将在创建该任务后的某一时间由系统启动。如果尝试在这样的任务上调用 Start,将引发异常。

2)        转化的APM异步模型必须符合两个模式:

a)        接受Begin***和End***方法。此时要求Begin***方法签名的委托必须是AsyncCallback以及 End***方法只接受IAsyncResult一个参数。此模式AsyncCallback回调由系统自动生成,主要工作是调用End***方法。

1
2
3
4
public Task<TResult> FromAsync<TArg1, TResult>(
      Func<TArg1, AsyncCallback, object, IAsyncResult> beginMethod
    , Func<IAsyncResult, TResult> endMethod, TArg1 arg1
    , object state, TaskCreationOptions creationOptions);

b)        接受IAsyncResult对象以及End***方法。此时Begin***方法的签名已经无关紧要只要(即:此模式支持传入自定义回调委托)能返回IAsyncResult的参数以及 End***方法只接受IAsyncResult一个参数。

1
2
public Task<TResult> FromAsync<TResult>(IAsyncResult asyncResult
    , Func<IAsyncResult, TResult> endMethod);

3)        当然,我们有时需要给客户提供统一的 Begin***() 和 End***() 调用方式,我们可以直接使用Task从零开始构造APM。即:在 Begin***() 创建并开启任务,并返回Task。因为Task是继承自IAsyncResult接口的,所以我们可以将其传递给 End***() 方法,并在此方法里面调用Result属性来等待任务完成。

4)        对于返回的Task,可以随时以任务延续的形式注册回调。

现在将在《APM异步编程模型》博文中展现的示例转化为TAP模式。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Task<int> CalculateAsync<TArg1, TArg2>(
      Func<TArg1, TArg2, AsyncCallback, object, IAsyncResult> beginMethod
    , AsyncCallback userCallback, TArg1 num1, TArg2 num2, object asyncState)
{
    IAsyncResult result = beginMethod(num1, num2, userCallback, asyncState);
    return Task.Factory.FromAsync<int>(result
            , EndCalculate, TaskCreationOptions.None);
}
 
public Task<int> CalculateAsync(int num1, int num2, object asyncState)
{
    return Task.Factory.FromAsync<int, int, int>(BeginCalculate, EndCalculate
            , num1, num2, asyncState, TaskCreationOptions.None);
}
  1. EAP转化为TAP

我们可以使用TaskCompletionSource<TResult>实例将EAP操作表示为一个Task<TResult>。

TaskCompletionSource<TResult>表示未绑定委托的Task<TResult>的制造者方,并通过TaskCompletionSource<TResult>.Task属性获取由此Tasks.TaskCompletionSource<TResult>创建的Task<TResult>。

注意,TaskCompletionSource<TResult>创建的任何任务将由TaskCompletionSource启动,因此,用户代码不应在该任务上调用 Start()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TaskCompletionSource<TResult>
{
    public TaskCompletionSource();
    // 使用指定的状态和选项创建一个TaskCompletionSource<TResult>。
    //   state: 要用作基础 Task<TResult>的AsyncState的状态。
    public TaskCompletionSource(object state, TaskCreationOptions creationOptions);
 
    // 获取由此Tasks.TaskCompletionSource<TResult>创建的Tasks.Task<TResult>。
    public Task<TResult> Task { get; }
 
    // 将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Canceled状态。
    public void SetCanceled();
    public bool TrySetCanceled();
 
    // 将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Faulted状态。
    public void SetException(Exception exception);
    public void SetException(IEnumerable<Exception> exceptions);
    public bool TrySetException(Exception exception);
    public bool TrySetException(IEnumerable<Exception> exceptions);
 
    // 尝试将基础Tasks.Task<TResult>转换为TaskStatus.RanToCompletion状态。
    public bool TrySetResult(TResult result);
    ……       
}

现在我将在《基于事件的异步编程模式(EAP)》博文中展现的BackgroundWorker2组件示例转化为TAP模式。

我们需要修改地方有:

1)        创建一个TaskCompletionSource<int>实例tcs;

2)        为tcs.Task返回的任务创建延续任务,延续任务中根据前面任务的IsCanceled、IsFaulted、Result等成员做逻辑;

3)        Completed事件,在这里面我们将设置返回任务的状态。

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    // 1、创建 TaskCompletionSource<TResult>
tcs = new TaskCompletionSource<int>();
worker2.RunWorkerCompleted += RunWorkerCompleted;
    // 2、注册延续
tcs.Task.ContinueWith(t =>
{
        if (t.IsCanceled)
            MessageBox.Show("操作已被取消");
        else if (t.IsFaulted)
            MessageBox.Show(t.Exception.GetBaseException().Message);
        else
            MessageBox.Show(String.Format("操作已完成,结果为:{0}", t.Result));
    }, TaskContinuationOptions.ExecuteSynchronously);
    // 3、运行异步任务
    worker2.RunWorkerAsync();
    // 4、Completed事件
    private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Error != null)
            tcs.SetException(e.Error);
        else if (e.Cancelled)
            tcs.SetCanceled();
        else
            tcs.SetResult((int)e.Result);
        // 注销事件,避免多次挂接事件
        worker2.RunWorkerCompleted -= RunWorkerCompleted;
    }

使用关键字async和await实现异步方法

在C#5.0中引入了async和await关键字,可以方便我们使用顺序结构流(即不用回调)来实现异步编程,大大降低了异步编程的复杂程度。(vs2010打Visual Studio Async CTP for VS2010补丁可以引入关键字”async”和”await”的支持,但是得不到.net4.5新增API的支持)

异步方法的实现原理

异步方法不需要多线程,因为一个异步方法并不是运行在一个独立的线程中的。

异步方法运行在当前同步上下文中,只有激活的时候才占用当前线程的时间。

异步模型采用时间片轮转来实现。

异步方法的参数和返回值

异步方法的参数:

不能使用“ref”参数和“out”参数,但是在异步方法内部可以调用含有这些参数的方法

异步方法的返回类型:

Task<TResult>:Tresult为异步方法的返回值类型。

Task:异步方法没有返回值。

void:主要用于事件处理程序(不能被等待,无法捕获异常)。异步事件通常被认为是一系列异步操作的开始。使用void返回类型不需要await,而且调用void异步方法的函数不会捕获方法抛出的异常。(异步事件中使用await,倘若等待的任务由有异常会导致抛出“调用的目标发生了异常”。当然你可以在异步事件中调用另一个有返回值的异步方法)

异步方法的命名规范

异步方法的方法名应该以Async作为后缀

事件处理程序,基类方法和接口方法,可以忽略此命名规范:例如: startButton_Click不应重命名为startButton_ClickAsync

async和await关键字不会导致其他线程的创建,执行异步方法的线程为其调用线程。而异步方法旨在成为非阻塞操作,即当await等待任务运行时,异步方法会将控制权转移给异步方法外部,让其不受阻塞的继续执行,待await等待的任务执行完毕再将控制权转移给await处,继续执行异步方法后续的代码。

  1. 我们可通过下图来明白异步方法的构建和异步方法的执行流程。(代码详见我提供的示例程序async_await_method项目)

需要注意的一个问题:被“async”关键字标记的方法的调用都会强制转变为异步方式吗?

不会,当你调用一个标记了”async”关键字的方法,它会在当前线程以同步的方式开始运行。所以,如果你有一个同步方法,它返回void并且你做的所有改变只是将其标记的“async”,这个方法调用依然是同步的。返回值为Task或Task<TResult>也一样。

方法用“async”关键字标记不会影响方法是同步还是异步运行并完成,而是,它使方法可被分割成多个片段,其中一些片段可能异步运行,这样这个方法可能异步完成。这些片段界限就出现在方法内部显示使用“await”关键字的位置处。所以,如果在标记了“async”的方法中没有显示使用“await”,那么该方法只有一个片段,并且将以同步方式运行并完成。

  1. 编译器转换

使用 async 关键字标记方法,会导致 C# 或 Visual Basic 编译器使用状态机重新编写该方法的实现。借助此状态机,编译器可以在该方法中插入多个中断点,以便该方法可以在不阻止线程的情况下,挂起和恢复其执行。这些中断点不会随意地插入。它们只会在您明确使用 await 关键字的位置插入:

1
2
3
4
5
6
private async void btnDoWork_Click(object sender, EventArgs e)
{
    ...
    await someObject; // <-- potential method suspension point
    ...
}

当您等待未完成的异步操作时,编译器生成的代码可确保与该方法相关的所有状态(例如,局部变量)封装并保留在堆中。然后,该函数将返回到调用程序,允许在其运行的线程中执行其他任务。当所等待的异步操作在稍后完成时,该方法将使用保留的状态恢复执行。

任何公开 await 模式的类型都可以进行等待。该模式主要由一个公开的 GetAwaiter()方法组成,该方法会返回一个提供 IsCompleted、OnCompleted 和 GetResult 成员的类型。当您编写以下代码时:

1
await someObject;

编译器会生成一个包含 MoveNext 方法的状态机类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private class FooAsyncStateMachine : IAsyncStateMachine
{
    // Member fields for preserving “locals” and other necessary     state
    int $state;
    TaskAwaiter $awaiter;
    
    public void MoveNext()
    {
        // Jump table to get back to the right statement upon         resumption
        switch (this.$state)
        {
            
        case 2: goto Label2;
            
        }
        
        // Expansion of “await someObject;”
        this.$awaiter = someObject.GetAwaiter();
        if (!this.$awaiter.IsCompleted)
        {
            this.$state = 2;
            this.$awaiter.OnCompleted(MoveNext);
            return;
            Label2:
        }
        this.$awaiter.GetResult();
        
    }
}

在实例someObject上使用这些成员来检查该对象是否已完成(通过 IsCompleted),如果未完成,则挂接一个续体(通过 OnCompleted),当所等待实例最终完成时,系统将再次调用 MoveNext 方法,完成后,来自该操作的任何异常将得到传播或作为结果返回(通过 GetResult),并跳转至上次执行中断的位置。

  1. 自定义类型支持等待

如果希望某种自定义类型支持等待,我们可以选择两种主要的方法。

1)        一种方法是针对自定义的可等待类型手动实现完整的 await 模式,提供一个返回自定义等待程序类型的 GetAwaiter 方法,该等待程序类型知道如何处理续体和异常传播等等。

2)        第二种实施该功能的方法是将自定义类型转换为Task,然后只需依靠对等待任务的内置支持来等待特殊类型。前文所展示的EAP转化为TAP正属于这一类,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private async void btn_Start_Click(object sender, EventArgs e)
{
    this.progressBar1.Value = 0;
 
    tcs = new TaskCompletionSource&lt;int&gt;();
    worker2.RunWorkerCompleted += RunWorkerCompleted;
    tcs.Task.ContinueWith(t =&gt;
    {
        if (t.IsCanceled)
            MessageBox.Show("操作已被取消");
        else if (t.IsFaulted)
            MessageBox.Show(t.Exception.GetBaseException().Message);
        else
            MessageBox.Show(String.Format("操作已完成,结果为:{0}", t.Result));
    }, TaskContinuationOptions.ExecuteSynchronously);
 
    worker2.RunWorkerAsync();
    // void的异步方法:主要用于事件处理程序(不能被等待,无法捕获异常)。异步事件通常被认为
    // 是一系列异步操作的开始。使用void返回类型不需要await,而且调用void异步方法的函数不
    // 会捕获方法抛出的异常。(异步事件中使用await,倘若等待的任务由有异常会导致
    // 抛出“调用的目标发生了异常”。当然你可以在异步事件中调用另一个有返回值的异步方法)
 
    // 所以不需要下面的await,因为会出现在执行取消后拖动界面会因异常被观察到并且终止整个进程
    // await tcs.Task;
}

处理TAP中的异常

在任务抛出的未处理异常都封装在System.AggregateException对象中。这个对象会存储在方法返回的Task或Task<TResult>对象中,需要通过访问Wait()、Result、Exception成员才能观察到异常。(所以,在访问Result之前,应先观察IsCanceled和IsFaulted属性)

  1. AggregateException对象的三个重要成员

1)        InnerExceptions属性

获取导致当前异常的System.Exception实例的只读集合(即,ReadOnlyCollection<Exception>)。不要将其与基类Exception提供的InnerException属性混淆。

2)        Flatten() 方法

遍历InnerExceptions异常列表,若列表中包含类型为AggregateException的异常,就移除所有嵌套的AggregateException,直接返回其真真的异常信息(效果如下图)。

1)        Handle(Func<Exception, bool> predicate)方法

它为AggregateException中包含的每个异常都调用一个回调方法。然后,回调方法可以为每个异常决定如何对其进行处理,回调返回true表示异常已经处理,返回false表示没有。在调用Handle之后,如果至少有一个异常没有处理,就创建一个新的AggregateException对象,其中只包含未处理的异常,并抛出这个新的AggregateException对象。

比如:将任何OperationCanceledException对象都视为已处理。其他任何异常都造成抛出一个新的AggregateException,其中只包含未处理的异常。

1
2
3
4
5
try{……}
catch (AggregateException ae)
{
    ae.Handle(e => e is OperationCanceledException);
}
  1. 父任务生成了多个子任务,而多个子任务都抛出了异常

1)        嵌套子任务

1
2
3
4
5
6
7
8
9
10
Task t4 = Task.Factory.StartNew(() =>
{
    Task.Factory.StartNew(() => { throw new Exception("子任务Exception_1"); }
            , TaskCreationOptions.AttachedToParent);
 
    Task.Factory.StartNew(() => { throw new Exception("子任务Exception_2"); }
            , TaskCreationOptions.AttachedToParent);
 
    throw new Exception("父任务Exception");
});

对于“嵌套子任务”中子任务的异常都会包装在父任务返回的Task或Task<TResult>对象中。如此例子中 t4.Exception.InnerExceptions的Count为3。

对于子任务返回的异常类型为包装过的AggregateException对象,为了避免循环访问子任务异常对象的InnerExceptions才能获取真真的异常信息,可以使用上面提到的Flatten() 方法移除所有嵌套的AggregateExceprion。

2)        Continue子任务

1
2
3
4
5
6
7
8
9
10
11
12
13
Task t1 = Task.Factory.StartNew(() =>
{
    Thread.Sleep(500);   // 确保已注册好延续任务
    throw new Exception("父任务Exception");
}, TaskCreationOptions.AttachedToParent);
Task t2 = t1.ContinueWith((t) =>
{
    throw new Exception("子任务Exception_1");
});
Task t3 = t1.ContinueWith((t) =>
{
    throw new Exception("子任务Exception_2");
});

对于“Continue子任务”中的子任务其异常与父任务是分离的,各自包装在自己返回的Task或 Task<TResult>对象中。如此示例 t1、t2、t3 的Exception.InnerExceptions的Count都为1。

  1. TaskScheduler的UnobservedTaskException事件

假如你一直不访问Task的Wait()、Result、Exception成员,那么你将永远注意不到这些异常的发生。为了帮助你检测到这些未处理的异常,可以向TaskScheduler对象的UnobservedTaskException事件注册回调函数。每当一个Task被垃圾回收时,如果存在一个没有注意到的异常,CLR的终结器线程会引发这个事件。

可在事件回调函数中调用UnobservedTaskExceptionEventArgs对象的SetObserved() 方法来指出已经处理好了异常,从而阻止CLR终止线程。然而并不推荐这么做,宁愿终止进程也不要带着已经损坏的状态继续运行。

示例代码:(要监控此代码必须在GC.Collect();和事件里两个地方进行断点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TaskScheduler.UnobservedTaskException += (s, e) =>
{
    //设置所有未觉察异常已被处理
    e.SetObserved();
};
Task.Factory.StartNew(() =>
{
    throw new Exception();
});
//确保任务完成
Thread.Sleep(100);
//强制垃圾会受到,在GC回收时才会触发UnobservedTaskException事件
GC.Collect();
//等待终结器处理
GC.WaitForPendingFinalizers();
  1. 返回void的async“异步方法”中的异常

我们已经知道返回Task或Task<TResult>对象的任务中抛出的异常会随着返回对象一起返回,可通过Exception属性获取。这对于返回Task或Task<TResult>对象的“异步方法”情况也是一样。

然而对于返回void的“异步方法”,方法中抛出的异常会直接导致程序奔溃。

1
2
3
4
public static async void Test_void_async_Exception()
{
    throw new Exception();
}

另外,我们还要特别注意lambda表达式构成的“异步方法”,如:

1
Enumerable.Range(0, 3).ToList().ForEach(async (i) => { throw new Exception(); });

本博文到此结束,我相信你看累了,其实我也写了很久…很久…,写完此文,我的“异步编程系列”也算有头有尾了(还会继续扩充)。本博文主要介绍了Task的重要API、任务的CLR线程池引擎、TaskFactory对象、TaskScheduler对象、TaskExtensions对象、AMP转化为TAP和EAP转化为TAP、使用关键字async和await实现异步方法以及自定义类型支持等待、处理TAP中的异常。

感谢你的观看,如果对你有帮助,还请多多推荐……

===================================================================

本篇博文基于.NET4.5中TPL所写。对于.NET4.0中TPL会有些差异,若有园友知道差异还请告知,我这边做个记录方便大家也方便自己。

1、.NET4.0中TPL未观察到的异常会在GC回收时终止进程。(园友:YamatAmain,讨论见21-26楼)

===================================================================

推荐阅读:

异步性能:了解 Async 和 Await 的成本-----有讲解到使用Task.ConfigureAwait(false)来避免捕获原上下文来提升性能。

关于async与await的FAQ      -----详细讲解了await和async的作用和意义,以及什么是可等待对象、等待者……(此文可帮助你解决80%关于await和async关键字的疑惑)

深入探究 WinRT 和 await      -----基于WinRT平板win8系统,讲解了异步功能API、TAP、编译器转换……

参考资料:MSDN

书籍:《CLR via C#(第三版)》

书籍:《C# 并行编程高级教程:精通.NET 4 Parallel Extensions》

分享到:
 

NET异步编程之新利器——Task与Await、Async

 

一.  FrameWork 4.0之前的线程世界    

    在.NET FrameWork 4.0之前,如果我们使用线程。一般有以下几种方式:

  • 使用System.Threading.Thread 类,调用实例方法Start()开启一个新线程,调用Abort()方法来提前终止线程。
  • 使用System.Threading.ThreadPool类,调用静态方法QueueUserWorkItem(),将方法放入线程池队列,线程池来控制调用。
  • 使用BeginInvoke,EndInvoke,BeginRead,EnRead,BeginWrite,EndWrite等一系列的异步方法。
  • 使用System.ComponentModel.BackgroundWorker控件,调用实例方法RunWorkerAsync(),开启一个新线程。 

二.  .Net 传统异步编程概述 

  • 异步编程模型 (APM),在该模型中异步操作由一对 Begin/End 方法(如 FileStream.BeginRead 和 Stream.EndRead)表示。
  • 基于事件的异步模式 (EAP),在该模式中异步操作由名为“操作名称Async”和“操作名称Completed”的方法/事件对(例如 WebClient.DownloadStringAsync 和 WebClient.DownloadStringCompleted)表示。 (EAP 是在 .NET Framework 2.0 版中引入的,在silverlight或者wpf变成中经常用到)。

三.  Task 的优点以及功能   

  • 在任务启动后,可以随时以任务延续的形式注册回调。
  • 通过使用 ContinueWhenAll 和 ContinueWhenAny 方法或者 WaitAll 方法或 WaitAny 方法,协调多个为了响应 Begin_ 方法而执行的操作。
  • 在同一 Task 对象中封装异步 I/O 绑定和计算绑定操作。
  • 监视 Task 对象的状态。
  • 使用 TaskCompletionSource 将操作的状态封送到 Task 对象。

四.  TASK的用法

     请参考这篇文章

五.  使用 Task 封装常见的异步编程模式

  • 使用Task封装APM异步编程模式。C#5.0中提供的async和await使异步编程更简单。await在msdn的解释是“运算符应用于一个异步方法的任务挂起方法的执行,直到等待任务完成。 任务表示正在进行的工作。”它返回的结果是Task和Task<TResult>.下面我们就用一个demo具体解析:
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
private async void Init()
{
//部分代码省略
var orgs = await _serviceClient.GetAllOrganizationTaskAsync();
} //通过WCF调用,采取分布类,名称一样。
public partial class ServiceClient
{
public Task<ObservableCollection<Organization>> GetAllOrganizationTaskAsync()
{
//Task 封装APM
return Task<ObservableCollection<Organization>>.Factory.FromAsync(this.Channel.BeginGetAllOrganization, this.Channel.EndGetAllOrganization, null);
}
}
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
  • 使用 Task 对象封装 EPM 异步模式 。 这种模式从 .Net 2.0 开始出现, 同时在 Silverlight 中大量出现, 这种异步模式以 “操作名称Async” 函数和 “操作名称Completed” 事件成对出现为特征。常见的操作有使用lamda表达式;或者使用+=“操作名称Completed”(tip:如果使用lamda表达式,无法回收资源,如果同时调用多个,数据会乱套,建议如果非要用,最好用+=“操作名称Completed”,在操作名称Completed事件中再-=“操作名称Completed”)。
    关键字:TaskCompletionSource,简单理解委托给task属性,利用task来操作。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
var source = new TaskCompletionSource<string>();

var webClient = new WebClient();

webClient.DownloadStringCompleted += (sender, args) => {

   if (args.Cancelled) {

      source.SetCanceled();

      return;

   }

   if (args.Error != null) {

      source.SetException(args.Error);

      return;

   }

   source.SetResult(args.Result);

};

webClient.DownloadStringAsync(new Uri(UrlToTest, UriKind.Absolute), null);

source.Task.Wait();

var result = source.Task.Result;
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

4种异步调用的模式,分为“等待”和“回调”两大类。四种方法,我在代码中都进行了详细的注释,这里不罗嗦了,直接用代码说明吧

第一种方法:BeginEnvoke EndEnvoke方法,属于“等待”类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading; namespace 异步调用实现方法汇总
{
///
/// 异步调用方法总结:
/// 1.BeginEnvoke EndEnvoke
/// 当使用BeginInvoke异步调用方法时,如果方法未执行完,EndInvoke方法就会一直阻塞,直到被调用的方法执行完毕
///
class Program
{
public delegate void PrintDelegate(string s);
static void Main(string[] args)
{
PrintDelegate printDelegate = Print;
Console.WriteLine("主线程"); IAsyncResult result= printDelegate.BeginInvoke("Hello World.", null, null);
Console.WriteLine("主线程继续执行...");
//当使用BeginInvoke异步调用方法时,如果方法未执行完,EndInvoke方法就会一直阻塞,直到被调用的方法执行完毕
printDelegate.EndInvoke(result); Console.WriteLine("Press any key to continue...");
Console.ReadKey(true);
} public static void Print(string s)
{
Console.WriteLine("异步线程开始执行:"+s);
Thread.Sleep(5000);
}
}
}
需要注意的地方,代码中都有注明了,程序运行结果如下:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

第二种方法:WaitOne。同样属于“等待”类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading; namespace 异步调用实现方法汇总2
{
///
/// 异步调用方法总结:
/// 2.WaitOne
/// 可以看到,与EndInvoke类似,只是用WaitOne函数代码了EndInvoke而已。
///
class Program
{
public delegate void PrintDelegate(string s);
static void Main(string[] args)
{
PrintDelegate printDelegate = Print;
Console.WriteLine("主线程");
IAsyncResult result = printDelegate.BeginInvoke("Hello World.", null, null);
Console.WriteLine("主线程继续执行...");
result.AsyncWaitHandle.WaitOne(-1, false); Console.WriteLine("Press any key to continue...");
Console.ReadKey(true);
}
public static void Print(string s)
{
Console.WriteLine("异步线程开始执行:" + s);
Thread.Sleep(5000);
}
}
}

需要注意的地方,代码中都有注明了,程序运行结果如下:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

第三种方法:轮询。也是属于“等待”类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading; namespace 异步调用实现方法汇总3
{
///
/// 异步调用方法总结:
/// 3.轮询
/// 之前提到的两种方法,只能等下异步方法执行完毕,
/// 在完毕之前没有任何提示信息,整个程序就像没有响应一样,用户体验不好,
/// 可以通过检查IasyncResult类型的IsCompleted属性来检查异步调用是否完成,
/// 如果没有完成,则可以适时地显示一些提示信息
///
class Program
{
public delegate void PrintDelegate(string s);
static void Main(string[] args)
{
PrintDelegate printDelegate = Print;
Console.WriteLine("主线程:"+Thread.CurrentThread.ManagedThreadId );
IAsyncResult result = printDelegate.BeginInvoke("Hello world.", null, null);
Console.WriteLine("主线程:" + Thread.CurrentThread.ManagedThreadId + ",继续执行...");
while (!result.IsCompleted)
{
Console.WriteLine(".");
Thread.Sleep(500);
} Console.WriteLine("主线程:" + Thread.CurrentThread.ManagedThreadId + " Press any key to continue...");
Console.ReadKey(true);
}
public static void Print(string s)
{
Console.WriteLine("当前线程:" + Thread.CurrentThread.ManagedThreadId + s);
Thread.Sleep(5000);
}
}
}

需要注意的地方,代码中都有注明了,程序运行结果如下:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

第四种方法:回调。当然属于“回调”类。推荐!!!!

  之前三种方法者在等待异步方法执行完毕后才能拿到执行的结果,期间主线程均处于等待状态。回调和它们最大的区别是,在调用BeginInvoke时只要提供了回调方法,那么主线程就不必要再等待异步线程工作完毕,异步线程在工作结束后会主动调用我们提供的回调方法,并在回调方法中做相应的处理。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading; namespace 异步调用实现方法汇总4
{
///
/// 异步调用方法总结:
/// 4.回调
/// 之前三种方法者在等待异步方法执行完毕后才能拿到执行的结果,期间主线程均处于等待状态。
/// 回调和它们最大的区别是,在调用BeginInvoke时只要提供了回调方法,那么主线程就不必要再等待异步线程工作完毕,
/// 异步线程在工作结束后会主动调用我们提供的回调方法,并在回调方法中做相应的处理,例如显示异步调用的结果。
///
class Program
{
public delegate void PrintDelegate(string s);
static void Main(string[] args)
{
PrintDelegate printDelegate = Print;
Console.WriteLine("主线程.");
printDelegate.BeginInvoke("Hello world.", PrintComeplete, printDelegate);
Console.WriteLine("主线程继续执行..."); Console.WriteLine("Press any key to continue...");
Console.ReadKey(true);
}
public static void Print(string s)
{
Console.WriteLine("当前线程:"+s);
Thread.Sleep(5000);
}
//回调方法要求
//1.返回类型为void
//2.只有一个参数IAsyncResult
public static void PrintComeplete(IAsyncResult result)
{
(result.AsyncState as PrintDelegate).EndInvoke(result);
Console.WriteLine("当前线程结束." + result.AsyncState.ToString());
}
}
}

需要注意的地方,代码中都有注明了,程序运行结果如下:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

以上就是四种实现异步调用函数的四种方法,说的很清楚了,就写这么多~

C#异步编程

1、什么是异步?

异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库。异步操作在主应用程序线程以外的线程中执行。应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行。

2、同步与异步的区别

同步(Synchronous):在执行某个操作时,应用程序必须等待该操作执行完成后才能继续执行。

异步(Asynchronous):在执行某个操作时,应用程序可在异步操作执行时继续执行。实质:异步操作,启动了新的线程,主线程与方法线程并行执行。

3、异步和多线程的区别

我们已经知道,异步的实质是开启了新的线程。它与多线程的区别是什么呢?

简单的说就是:异步线程是由线程池负责管理,而多线程,我们可以自己控制,当然在多线程中我们也可以使用线程池。

就拿网络扒虫而言,如果使用异步模式去实现,它使用线程池进行管理。异步操作执行时,会将操作丢给线程池中的某个工作线程来完成。当开始I/O操作的时候,异步会将工作线程还给线程池,这意味着获取网页的工作不会再占用任何CPU资源了。直到异步完成,即获取网页完毕,异步才会通过回调的方式通知线程池。可见,异步模式借助于线程池,极大地节约了CPU的资源。

注:DMA(Direct Memory Access)直接内存存取,顾名思义DMA功能就是让设备可以绕过处理器,直接由内存来读取资料。通过直接内存访问的数据交换几乎可以不损耗CPU的资源。在硬件中,硬盘、网卡、声卡、显卡等都有直接内存访问功能。异步编程模型就是让我们充分利用硬件的直接内存访问功能来释放CPU的压力。
      两者的应用场景:

计算密集型工作,采用多线程。
      IO密集型工作,采用异步机制。

4、异步应用

.NET Framework 的许多方面都支持异步编程功能,这些方面包括:
      1)文件 IO、流 IO、套接字 IO。
      2)网络。
      3)远程处理信道(HTTP、TCP)和代理。
      4)使用 ASP.NET 创建的 XML Web services。
      5)ASP.NET Web 窗体。
      6)使用 MessageQueue 类的消息队列。

.NET Framework 为异步操作提供两种设计模式:
      1)使用 IAsyncResult 对象的异步操作。
      2)使用事件的异步操作。
      IAsyncResult 设计模式允许多种编程模型,但更加复杂不易学习,可提供大多数应用程序都不要求的灵活性。可能的话,类库设计者应使用事件驱动模型实现异步方法。在某些情况下,库设计者还应实现基于 IAsyncResult 的模型。

使用 IAsyncResult 设计模式的异步操作是通过名为 Begin操作名称和End操作名称的两个方法来实现的,这两个方法分别开始和结束异步操作操作名称。例如,FileStream 类提供 BeginRead 和 EndRead 方法来从文件异步读取字节。这两个方法实现了 Read 方法的异步版本。在调用 Begin操作名称后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。每次调用 Begin操作名称 时,应用程序还应调用 End操作名称来获取操作的结果。Begin操作名称 方法开始异步操作操作名称并返回一个实现 IAsyncResult 接口的对象。 .NET Framework 允许您异步调用任何方法。定义与您需要调用的方法具有相同签名的委托;公共语言运行库将自动为该委托定义具有适当签名的 BeginInvoke 和 EndInvoke 方法。

IAsyncResult 对象存储有关异步操作的信息。下表提供了有关异步操作的信息。

名称        

说明

AsyncState

获取用户定义的对象,它限定或包含关于异步操作的信息。

AsyncWaitHandle

获取用于等待异步操作完成的 WaitHandle。

CompletedSynchronously

获取一个值,该值指示异步操作是否同步完成。

IsCompleted

获取一个值,该值指示异步操作是否已完

5、应用实例

案例1-读取文件

通常读取文件是一个比较耗时的工作,特别是读取大文件的时候,常见的上传和下载。但是我们又不想让用户一直等待,用户同样可以进行其他操作,可以使得系统有良好的交互性。这里我们写了同步调用和异步调用来进行比较说明。

读取文件类

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
using System;
using System.IO;
using System.Threading; namespace AsynSample
{
class FileReader
{
/// <summary>
/// 缓存池
/// </summary>
private byte[] Buffer { get; set; }
/// <summary>
/// 缓存区大小
/// </summary>
public int BufferSize { get; set; } public FileReader(int bufferSize)
{
this.BufferSize = bufferSize;
this.Buffer = new byte[BufferSize];
} /// <summary>
/// 同步读取文件
/// </summary>
/// <param name="path">文件路径</param>
public void SynsReadFile(string path)
{
Console.WriteLine("同步读取文件 begin");
using (FileStream fs = new FileStream(path, FileMode.Open))
{
fs.Read(Buffer, 0, BufferSize);
string output = System.Text.Encoding.UTF8.GetString(Buffer);
Console.WriteLine("读取的文件信息:{0}",output);
} Console.WriteLine("同步读取文件 end");
}
/// <summary>
/// 异步读取文件
/// </summary>
/// <param name="path"></param>
public void AsynReadFile(string path)
{
Console.WriteLine("异步读取文件 begin");
//执行Endread时报错,fs已经释放,注意在异步中不能使用释放需要的资源
//using (FileStream fs = new FileStream(path, FileMode.Open))
//{
// Buffer = new byte[BufferSize];
// fs.BeginRead(Buffer, 0, BufferSize, AsyncReadCallback, fs);
//}
if (File.Exists(path))
{
FileStream fs = new FileStream(path, FileMode.Open);
fs.BeginRead(Buffer, 0, BufferSize, AsyncReadCallback, fs);
}
else
{
Console.WriteLine("该文件不存在");
} }
/// <summary>
///
/// </summary>
/// <param name="ar"></param>
void AsyncReadCallback(IAsyncResult ar)
{
FileStream stream = ar.AsyncState as FileStream;
if (stream != null)
{
Thread.Sleep(1000);
//读取结束
stream.EndRead(ar);
stream.Close(); string output = System.Text.Encoding.UTF8.GetString(this.Buffer);
Console.WriteLine("读取的文件信息:{0}", output);
}
}
}
}
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

测试用例

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
using System;
using System.Threading; namespace AsynSample
{
class Program
{
static void Main(string[] args)
{
FileReader reader = new FileReader(1024); //改为自己的文件路径
string path = "C:\\Windows\\DAI.log"; Console.WriteLine("开始读取文件了...");
//reader.SynsReadFile(path); reader.AsynReadFile(path); Console.WriteLine("我这里还有一大滩事呢.");
DoSomething();
Console.WriteLine("终于完事了,输入任意键,歇着!");
Console.ReadKey();
}
/// <summary>
///
/// </summary>
static void DoSomething()
{
Thread.Sleep(1000);
for (int i = 0; i < 10000; i++)
{
if (i % 888 == 0)
{
Console.WriteLine("888的倍数:{0}",i);
}
}
}
}
}
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

输出结果:

同步输出:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

异步输出:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

结果分析:

如果是同步读取,在读取时,当前线程读取文件,只能等到读取完毕,才能执行以下的操作

而异步读取,是创建了新的线程,读取文件,而主线程,继续执行。我们可以开启任务管理器来进行监视。

案例二--基于委托的异步操作

系统自带一些类具有异步调用方式,如何使得自定义对象也具有异步功能呢?

我们可以借助委托来轻松实现异步。

说到BeginInvoke,EndInvoke就不得不停下来看一下委托的本质。为了便于理解委托,我定义一个简单的委托:

public delegate string MyFunc(int num, DateTime dt);

我们再来看一下这个委托在编译后的程序集中是个什么样的:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

委托被编译成一个新的类型,拥有BeginInvoke,EndInvoke,Invoke这三个方法。前二个方法的组合使用便可实现异步调用。第三个方法将以同步的方式调用。 其中BeginInvoke方法的最后二个参数用于回调,其它参数则与委托的包装方法的输入参数是匹配的。 EndInvoke的返回值与委托的包装方法的返回值匹配。

异步实现文件下载:

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
using System;
using System.Text; namespace AsynSample
{
/// <summary>
/// 下载委托
/// </summary>
/// <param name="fileName"></param>
public delegate string AysnDownloadDelegate(string fileName);
/// <summary>
/// 通过委托实现异步调用
/// </summary>
class DownloadFile
{
/// <summary>
/// 同步下载
/// </summary>
/// <param name="fileName"></param>
public string Downloading(string fileName)
{
string filestr = string.Empty;
Console.WriteLine("下载事件开始执行");
System.Threading.Thread.Sleep(3000);
Random rand = new Random();
StringBuilder builder =new StringBuilder();
int num;
for(int i=0;i<100;i++)
{
num = rand.Next(1000);
builder.Append(i);
}
filestr = builder.ToString();
Console.WriteLine("下载事件执行结束"); return filestr;
}
/// <summary>
/// 异步下载
/// </summary>
public IAsyncResult BeginDownloading(string fileName)
{
string fileStr = string.Empty;
AysnDownloadDelegate downloadDelegate = new AysnDownloadDelegate(Downloading);
return downloadDelegate.BeginInvoke(fileName, Downloaded, downloadDelegate);
}
/// <summary>
/// 异步下载完成后事件
/// </summary>
/// <param name="result"></param>
private void Downloaded(IAsyncResult result)
{
AysnDownloadDelegate aysnDelegate = result.AsyncState as AysnDownloadDelegate;
if (aysnDelegate != null)
{
string fileStr = aysnDelegate.EndInvoke(result);
if (!string.IsNullOrEmpty(fileStr))
{
Console.WriteLine("下载文件:{0}", fileStr);
}
else
{
Console.WriteLine("下载数据为空!");
}
}
else
{
Console.WriteLine("下载数据为空!");
}
}
}
}
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

通过案例,我们发现,使用委托能够很轻易的实现异步。这样,我们就可以自定义自己的异步操作了。

System.Threading 命名空间

System.Threading 命名空间提供一些使得可以进行多线程编程的类和接口。 除同步线程活动和数据访问的类(Monitor、Interlocked、AutoResetEvent 等)之外,此命名空间还包含一个 ThreadPool 类(它使用户能够使用系统提供的线程池)和一个 Timer 类(在线程池线程上执行回调方法)。 除了 System.Threading 命名空间中提供的功能之外,BackgroundWorker 类还提供一个简单的基于事件的方法,以同步对主应用程序线程的访问。

Thread 类

一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码。 托管线程的执行单位是方法。 使用 ThreadStart 委托或ParameterizedThreadStart 委托可以指定由线程执行的方法。 使用 ParameterizedThreadStart 委托可以将数据作为参数传递到线程过程。

您不需要自己创建线程。 BackgroundWorker 和 ThreadPool 类使您可以通过一种面向任务的简单方式来使用系统管理的后台线程。 对于将结果返回用户界面 (UI) 线程的后台任务,最简单的编程方法是使用 BackgroundWorker 类。 下表列出了有关各种并发编程的一些信息来源。

任务

请参见

执行使用事件来与主应用程序线程通信的后台任务。

BackgroundWorker

执行不需要与主应用程序线程通信的后台任务。

托管线程池

保护代码或字段区域,使其不被并发访问。

Monitor;Visual Basic SyncLock 语句(在 C# 中为 lock 语句)

同步多个线程的活动。

EventWaitHandle、AutoResetEvent 和 ManualResetEvent

按固定的时间间隔在后台执行代码。

Timer

按固定的时间间隔在 UI 线程上执行代码。

DispatcherTimer

提供对数据的无锁并发访问。

互锁操作

创建您自己的线程。

Thread 类;启动时创建线程并传递数据

ThreadPool 类

提供一个线程池,该线程池可用于发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。线程池通过为应用程序提供一个由系统管理的辅助线程池,使您可以更为有效地使用线程。 一个线程监视排到线程池的若干个等待操作的状态。 当一个等待操作完成时,线程池中的一个辅助线程就会执行对应的每个进程都有一个线程池。 线程池的默认大小为:每个处理器 250 个辅助线程,再加上 1000 个 I/O 完成线程。

Just have a little faith.

C# Lock关键字

lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。

lock语句根本使用的就是Monitor.Enter和Monitor.Exit,也就是说lock(this)时执行Monitor.Enter(this),大括号结束时执行Monitor.Exit(this).他的意义在于什么呢,对于任何一个对象来说,他在内存中的第一部分放置的是所有方法的地址,第二部分放着一个索引,他指向CLR中的SyncBlock Cache区域中的一个SyncBlock.什么意思呢?就是说,当你执行Monitor.Enter(Object)时,如果object的索引值为负数,就从SyncBlock Cache中选区一个SyncBlock,将其地址放在object的索引中。这样就完成了以object为标志的锁定,其他的线程想再次进行Monitor.Enter(object)操作,将获得object为正数的索引,然后就等待。直到索引变为负数,即线程使用Monitor.Exit(object)将索引变为负数。

使用lock需要注意的地方:

1.lock不能锁定空值某一对象可以指向Null,但Null是不需要被释放的。(请参考:认识全面的null)
2.lock不能锁定string类型,虽然它也是引用类型的。因为字符串类型被CLR“暂留”

这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。
3.lock锁定的对象是一个程序块的内存边界
4.值类型不能被lock,因为前文标红字的“对象被释放”,值类型不是引用类型的

5.lock就避免锁定public 类型或不受程序控制的对象。
例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。
使用lock(this)的时候,类的成员变量的值可能会被不在临界区的方法改值了

应用场景:经常会应用于防止多线程操作导致公用变量值出现不确定的异常,用于确保操作的安全性

示例

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
// statements_lock2.cs
using System;
using System.Threading; class Account
{
private Object thisLock = new Object();
int balance; Random r = new Random(); public Account(int initial)
{
balance = initial;
} int Withdraw(int amount)
{ // This condition will never be true unless the lock statement
// is commented out:
if (balance < 0)
{
throw new Exception("Negative Balance");
} // Comment out the next line to see the effect of leaving out
// the lock keyword:
lock(thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : " + balance);
Console.WriteLine("Amount to Withdraw : -" + amount);
balance = balance - amount;
Console.WriteLine("Balance after Withdrawal : " + balance);
return amount;
}
else
{
return 0; // transaction rejected
}
}
} public void DoTransactions()
{
for (int i = 0; i < 100; i++)
{
Withdraw(r.Next(1, 100));
}
}
} class Test
{
static void Main()
{
Thread[] threads = new Thread[10];
Account acc = new Account(1000);
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++)
{
threads[i].Start();
}
}
}
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

C# 理解lock

 

一. 为什么要lock,lock了什么?

当我们使用线程的时候,效率最高的方式当然是异步,即各个线程同时运行,其间不相互依赖和等待。但当不同的线程都需要访问某个资源的时候,就需要同步机制了,也就是说当对同一个资源进行读写的时候,我们要使该资源在同一时刻只能被一个线程操作,以确保每个操作都是有效即时的,也即保证其操作的原子性。lock是C#中最常用的同步方式,格式为lock(objectA){codeB} 。

lock(objectA){codeB} 看似简单,实际上有三个意思,这对于适当地使用它至关重要:
1. objectA被lock了吗?没有则由我来lock,否则一直等待,直至objectA被释放。
2. lock以后在执行codeB的期间其他线程不能调用codeB,也不能使用objectA。
3. 执行完codeB之后释放objectA,并且codeB可以被其他线程访问。

二. lock(this)怎么了?

我们看一个例子:

  1. using System;
  2. using System.Threading;
  3. namespace Namespace1
  4. {
  5. class C1
  6. {
  7. private bool deadlocked = true;
  8. //这个方法用到了lock,我们希望lock的代码在同一时刻只能由一个线程访问
  9. public void LockMe(object o)
  10. {
  11. lock (this)
  12. {
  13. while(deadlocked)
  14. {
  15. deadlocked = (bool)o;
  16. Console.WriteLine("Foo: I am locked :(");
  17. Thread.Sleep(500);
  18. }
  19. }
  20. }
  21. //所有线程都可以同时访问的方法
  22. public void DoNotLockMe()
  23. {
  24. Console.WriteLine("I am not locked :)");
  25. }
  26. }
  27. class Program
  28. {
  29. static void Main(string[] args)
  30. {
  31. C1 c1 = new C1();
  32. //在t1线程中调用LockMe,并将deadlock设为true(将出现死锁)
  33. Thread t1 = new Thread(c1.LockMe);
  34. t1.Start(true);
  35. Thread.Sleep(100);
  36. //在主线程中lock c1
  37. lock (c1)
  38. {
  39. //调用没有被lock的方法
  40. c1.DoNotLockMe();
  41. //调用被lock的方法,并试图将deadlock解除
  42. c1.LockMe(false);
  43. }
  44. }
  45. }

复制代码

在t1线程中,LockMe调用了lock(this), 也就是Main函数中的c1,这时候在主线程中调用lock(c1)时,必须要等待t1中的lock块执行完毕之后才能访问c1,即所有c1相关的操作都无法完成,于是我们看到连c1.DoNotLockMe()都没有执行。

把C1的代码稍作改动:

  1. class C1
  2. {
  3. private bool deadlocked = true;
  4. private object locker = new object();
  5. //这个方法用到了lock,我们希望lock的代码在同一时刻只能由一个线程访问
  6. public void LockMe(object o)
  7. {
  8. lock (locker)
  9. {
  10. while(deadlocked)
  11. {
  12. deadlocked = (bool)o;
  13. Console.WriteLine("Foo: I am locked :(");
  14. Thread.Sleep(500);
  15. }
  16. }
  17. }
  18. //所有线程都可以同时访问的方法
  19. public void DoNotLockMe()
  20. {
  21. Console.WriteLine("I am not locked :)");
  22. }
  23. }

复制代码

这次我们使用一个私有成员作为锁定变量(locker),在LockMe中仅仅锁定这个私有locker,而不是整个对象。这时候重新运行程序,可以看到虽然t1出现了死锁,DoNotLockMe()仍然可以由主线程访问;LockMe()依然不能访问,原因是其中锁定的locker还没有被t1释放。

关键点:
1. lock(this)的缺点就是在一个线程(例如本例的t1)通过执行该类的某个使用"lock(this)"的方法(例如本例的LockMe())锁定某对象之后, 导致整个对象无法被其他线程(例如本例的主线程)访问 - 因为很多人在其他线程(例如本例的主线程)中使用该类的时候会使用类似lock(c1)的代码。
2. 锁定的不仅仅是lock段里的代码,锁本身也是线程安全的。
3. 我们应该使用不影响其他操作的私有对象作为locker。
4. 在使用lock的时候,被lock的对象(locker)一定要是引用类型的,如果是值类型,将导致每次lock的时候都会将该对象装箱为一个新的引用对象(事实上如果使用值类型,C#编译器(3.5.30729.1)在编译时就会给出一个错误)。

 

C#中的yield return

在迭代器块中用于向枚举数对象提供值或发出迭代结束信号。

yield return <expression>; yield break;

计算表达式并以枚举数对象值的形式返回;expression 必须可以隐式转换为迭代器的 yield 类型。

yield 语句只能出现在 iterator 块中,该块可用作方法、运算符或访问器的体。

一个迭代器块(iterator block)是一个能够产生有序的值序列的块。迭代器块和普通语句块的区别就是其中出现的一个或多个yield语句。 yield return语句产生迭代的下一个值。 yield break语句表示迭代完成。

示例

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
using System;
using System.Collections;
public class List
{
public static IEnumerable Power(int number, int exponent)
{
int counter = 0;
int result = 1;
while (counter++ < exponent)
{
result = result * number;
yield return result;
}
} static void Main()
{
// Display powers of 2 up to the exponent 8:
foreach (int i in Power(2, 8))
{
Console.Write("{0} ", i);
}
}
} 2 4 8 16 32 64 128 256
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

BackgroundWorker 类

在单独的线程上运行操作。

BackgroundWorker 类允许您在单独的专用线程上运行操作。 诸如下载和数据库事务这样的耗时操作会导致用户界面停止响应。 如果您需要能进行响应的用户界面,而且必须执行耗时操作,则可以使用 BackgroundWorker 类方便地解决问题。

若要在后台运行操作,请创建一个 BackgroundWorker。 可以侦听报告操作进度并在操作完成时发出信号的事件。

若要设置后台操作,请为 DoWork 事件添加事件处理程序。 在此事件处理程序中调用耗时的操作。 若要启动后台操作,请调用RunWorkerAsync 方法。 若要收到进度更新的通知,请处理 ProgressChanged 事件。 若要在操作完成时收到通知,请处理RunWorkerCompleted 事件。

说明:

您必须非常小心,确保在 DoWork 事件处理程序中不操作任何用户界面对象。 而应该通过 ProgressChanged 和 RunWorkerCompleted事件与用户界面进行通信。

如果后台操作需要参数,请在调用 RunWorkerAsync 时给出参数。 在 DoWork 事件处理程序内部,可以从DoWorkEventArgs.Argument 属性中提取该参数。

有关 BackgroundWorker 的更多信息,请参见如何:使用后台辅助线程

BackgroundWorker 类

Visual Studio 2010
 
其他版本
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程
 

更新:2010 年 9 月

在单独的线程上执行操作。

System.Object
  System.MarshalByRefObject
    System.ComponentModel.Component
      System.ComponentModel.BackgroundWorker

命名空间:  System.ComponentModel
程序集:  System(在 System.dll 中)

语法
 
 
 
[HostProtectionAttribute(SecurityAction.LinkDemand, SharedState = true)]
public class BackgroundWorker : Component

BackgroundWorker 类型公开以下成员。

构造函数
 
 
显示: 继承 保护
  名称 说明
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 BackgroundWorker 初始化 BackgroundWorker 类的新实例。

页首

属性
 
 
显示: 继承 保护
  名称 说明
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 CancellationPending 获取一个值,指示应用程序是否已请求取消后台操作。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 CanRaiseEvents 获取一个指示组件是否可以引发事件的值。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Container 获取 IContainer,它包含 Component。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 DesignMode 获取一个值,用以指示 Component 当前是否处于设计模式。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Events 获取附加到此 Component 的事件处理程序的列表。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 IsBusy 获取一个值,指示 BackgroundWorker 是否正在运行异步操作。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Site 获取或设置 Component 的 ISite。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 WorkerReportsProgress 获取或设置一个值,该值指示 BackgroundWorker 能否报告进度更新。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 WorkerSupportsCancellation 获取或设置一个值,该值指示 BackgroundWorker 是否支持异步取消。

页首

方法
 
 
显示: 继承 保护
  名称 说明
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 CancelAsync 请求取消挂起的后台操作。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 CreateObjRef 创建一个对象,该对象包含生成用于与远程对象进行通信的代理所需的全部相关信息。 (继承自MarshalByRefObject。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Dispose() 释放由 Component 使用的所有资源。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Dispose(Boolean) 释放 Component 占用的非托管资源,也可以选择释放托管资源。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Equals(Object) 确定指定的 Object 是否等于当前的 Object。 (继承自 Object。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Finalize 在通过垃圾回收将 Component 回收之前,释放非托管资源并执行其他清理操作。 (继承自Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 GetHashCode 用作特定类型的哈希函数。 (继承自 Object。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 GetLifetimeService 检索控制此实例的生存期策略的当前生存期服务对象。 (继承自 MarshalByRefObject。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 GetService 返回一个对象,该对象表示由 Component 或它的 Container 提供的服务。 (继承自Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 GetType 获取当前实例的 Type。 (继承自 Object。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 InitializeLifetimeService 获取控制此实例的生存期策略的生存期服务对象。 (继承自 MarshalByRefObject。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 MemberwiseClone() 创建当前 Object 的浅表副本。 (继承自 Object。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 MemberwiseClone(Boolean) 创建当前 MarshalByRefObject 对象的浅表副本。 (继承自 MarshalByRefObject。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 OnDoWork 引发 DoWork 事件。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 OnProgressChanged 引发 ProgressChanged 事件。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 OnRunWorkerCompleted 引发 RunWorkerCompleted 事件。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 ReportProgress(Int32) 引发 ProgressChanged 事件。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 ReportProgress(Int32, Object) 引发 ProgressChanged 事件。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 RunWorkerAsync() 开始执行后台操作。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 RunWorkerAsync(Object) 开始执行后台操作。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 ToString 返回包含 Component 的名称的 String(如果有)。 不应重写此方法。 (继承自 Component。)

页首

事件
 
 
显示: 继承 保护
  名称 说明
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 Disposed 当通过调用 Dispose 方法释放组件时发生。 (继承自 Component。)
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 DoWork 调用 RunWorkerAsync 时发生。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 ProgressChanged 调用 ReportProgress 时发生。
【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程 RunWorkerCompleted 当后台操作已完成、被取消或引发异常时发生。

页首

备注
 
 

BackgroundWorker 类允许您在单独的专用线程上运行操作。 耗时的操作(如下载和数据库事务)在长时间运行时可能会导致用户界面 (UI) 似乎处于停止响应状态。 如果您需要能进行响应的用户界面,而且面临与这类操作相关的长时间延迟,则可以使用 BackgroundWorker 类方便地解决问题。

若要在后台执行耗时的操作,请创建一个 BackgroundWorker,侦听那些报告操作进度并在操作完成时发出信号的事件。 可以通过编程方式创建BackgroundWorker,也可以将它从“工具箱”的“组件”选项卡中拖到窗体上。 如果在 Windows 窗体设计器中创建 BackgroundWorker,则它会出现在组件栏中,而且它的属性会显示在“属性”窗口中。

若要为后台操作做好准备,请添加 DoWork 事件的事件处理程序。 在此事件处理程序中调用耗时的操作。 若要开始此操作,请调用RunWorkerAsync。 若要收到进度更新的通知,请处理 ProgressChanged 事件。 若要在操作完成时收到通知,请处理 RunWorkerCompleted 事件。

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程注意

您必须非常小心,确保在 DoWork 事件处理程序中不操作任何用户界面对象。 而应该通过 ProgressChanged 和 RunWorkerCompleted 事件与用户界面进行通信。

BackgroundWorker 事件不跨 AppDomain 边界进行封送处理。 请不要使用 BackgroundWorker 组件在多个 AppDomain 中执行多线程操作。

如果后台操作需要参数,请在调用 RunWorkerAsync 时给出参数。 在 DoWork 事件处理程序内部,可以从 DoWorkEventArgs.Argument 属性中提取该参数。

有关 BackgroundWorker的更多信息,请参见如何:在后台运行操作

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程注意

应用到此类型或成员的 HostProtectionAttribute 特性具有以下 Resources 属性值:SharedState。HostProtectionAttribute 不影响桌面应用程序(桌面应用程序一般通过双击图标、键入命令或在浏览器中输入 URL 启动)。有关更多信息,请参见 HostProtectionAttribute 类或 SQL Server 编程和宿主保护特性

示例
 
 

下面的代码示例演示 BackgroundWorker 类异步执行耗时的基本知识。 下图显示输出的示例。

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

要尝试该代码,可创建 Windows 窗体应用程序。 添加一个名为 resultLabel 的 Label 控件并添加两个名为 startAsyncButton 和cancelAsyncButton 的 Button 控件。 创建这两个按钮的 Click 事件处理程序。 从工具箱中的“组件”选项卡中,添加命名为 backgroundWorker1的 BackgroundWorker 组件。 创建 DoWork、 ProgressChanged 和 BackgroundWorker 的 RunWorkerCompleted 事件处理程序。 在窗体的代码中,用下列代码替换现有代码。

 
using System;
using System.ComponentModel;
using System.Windows.Forms; namespace BackgroundWorkerSimple
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
} private void startAsyncButton_Click(object sender, EventArgs e)
{
if (backgroundWorker1.IsBusy != true)
{
// Start the asynchronous operation.
backgroundWorker1.RunWorkerAsync();
}
} private void cancelAsyncButton_Click(object sender, EventArgs e)
{
if (backgroundWorker1.WorkerSupportsCancellation == true)
{
// Cancel the asynchronous operation.
backgroundWorker1.CancelAsync();
}
} // This event handler is where the time-consuming work is done.
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker; for (int i = 1; i <= 10; i++)
{
if (worker.CancellationPending == true)
{
e.Cancel = true;
break;
}
else
{
// Perform a time consuming operation and report progress.
System.Threading.Thread.Sleep(500);
worker.ReportProgress(i * 10);
}
}
} // This event handler updates the progress.
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
resultLabel.Text = (e.ProgressPercentage.ToString() + "%");
} // This event handler deals with the results of the background operation.
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled == true)
{
resultLabel.Text = "Canceled!";
}
else if (e.Error != null)
{
resultLabel.Text = "Error: " + e.Error.Message;
}
else
{
resultLabel.Text = "Done!";
}
}
}
}

下面的代码示例演示如何使用 BackgroundWorker 类异步执行耗时的操作。 下图显示输出的示例。

【温故而知新-万花筒】C# 异步编程 逆变  协变 委托 事件  事件参数 迭代 线程、多线程、线程池、后台线程

该操作计算选定的斐波纳契数,在计算过程中报告进度更新,并允许取消挂起的计算。

 
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms; namespace BackgroundWorkerExample
{
public class FibonacciForm : System.Windows.Forms.Form
{
private int numberToCompute = 0;
private int highestPercentageReached = 0; private System.Windows.Forms.NumericUpDown numericUpDown1;
private System.Windows.Forms.Button startAsyncButton;
private System.Windows.Forms.Button cancelAsyncButton;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label resultLabel;
private System.ComponentModel.BackgroundWorker backgroundWorker1; public FibonacciForm()
{
InitializeComponent(); InitializeBackgroundWorker();
} // Set up the BackgroundWorker object by
// attaching event handlers.
private void InitializeBackgroundWorker()
{
backgroundWorker1.DoWork +=
new DoWorkEventHandler(backgroundWorker1_DoWork);
backgroundWorker1.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(
backgroundWorker1_RunWorkerCompleted);
backgroundWorker1.ProgressChanged +=
new ProgressChangedEventHandler(
backgroundWorker1_ProgressChanged);
} private void startAsyncButton_Click(System.Object sender,
System.EventArgs e)
{
// Reset the text in the result label.
resultLabel.Text = String.Empty; // Disable the UpDown control until
// the asynchronous operation is done.
this.numericUpDown1.Enabled = false; // Disable the Start button until
// the asynchronous operation is done.
this.startAsyncButton.Enabled = false; // Enable the Cancel button while
// the asynchronous operation runs.
this.cancelAsyncButton.Enabled = true; // Get the value from the UpDown control.
numberToCompute = (int)numericUpDown1.Value; // Reset the variable for percentage tracking.
highestPercentageReached = 0; // Start the asynchronous operation.
backgroundWorker1.RunWorkerAsync(numberToCompute);
} private void cancelAsyncButton_Click(System.Object sender,
System.EventArgs e)
{
// Cancel the asynchronous operation.
this.backgroundWorker1.CancelAsync(); // Disable the Cancel button.
cancelAsyncButton.Enabled = false;
} // This event handler is where the actual,
// potentially time-consuming work is done.
private void backgroundWorker1_DoWork(object sender,
DoWorkEventArgs e)
{
// Get the BackgroundWorker that raised this event.
BackgroundWorker worker = sender as BackgroundWorker; // Assign the result of the computation
// to the Result property of the DoWorkEventArgs
// object. This is will be available to the
// RunWorkerCompleted eventhandler.
e.Result = ComputeFibonacci((int)e.Argument, worker, e);
} // This event handler deals with the results of the
// background operation.
private void backgroundWorker1_RunWorkerCompleted(
object sender, RunWorkerCompletedEventArgs e)
{
// First, handle the case where an exception was thrown.
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
}
else if (e.Cancelled)
{
// Next, handle the case where the user canceled
// the operation.
// Note that due to a race condition in
// the DoWork event handler, the Cancelled
// flag may not have been set, even though
// CancelAsync was called.
resultLabel.Text = "Canceled";
}
else
{
// Finally, handle the case where the operation
// succeeded.
resultLabel.Text = e.Result.ToString();
} // Enable the UpDown control.
this.numericUpDown1.Enabled = true; // Enable the Start button.
startAsyncButton.Enabled = true; // Disable the Cancel button.
cancelAsyncButton.Enabled = false;
} // This event handler updates the progress bar.
private void backgroundWorker1_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
this.progressBar1.Value = e.ProgressPercentage;
} // This is the method that does the actual work. For this
// example, it computes a Fibonacci number and
// reports progress as it does its work.
long ComputeFibonacci(int n, BackgroundWorker worker, DoWorkEventArgs e)
{
// The parameter n must be >= 0 and <= 91.
// Fib(n), with n > 91, overflows a long.
if ((n < 0) || (n > 91))
{
throw new ArgumentException(
"value must be >= 0 and <= 91", "n");
} long result = 0; // Abort the operation if the user has canceled.
// Note that a call to CancelAsync may have set
// CancellationPending to true just after the
// last invocation of this method exits, so this
// code will not have the opportunity to set the
// DoWorkEventArgs.Cancel flag to true. This means
// that RunWorkerCompletedEventArgs.Cancelled will
// not be set to true in your RunWorkerCompleted
// event handler. This is a race condition. if (worker.CancellationPending)
{
e.Cancel = true;
}
else
{
if (n < 2)
{
result = 1;
}
else
{
result = ComputeFibonacci(n - 1, worker, e) +
ComputeFibonacci(n - 2, worker, e);
} // Report progress as a percentage of the total task.
int percentComplete =
(int)((float)n / (float)numberToCompute * 100);
if (percentComplete > highestPercentageReached)
{
highestPercentageReached = percentComplete;
worker.ReportProgress(percentComplete);
}
} return result;
} #region Windows Form Designer generated code private void InitializeComponent()
{
this.numericUpDown1 = new System.Windows.Forms.NumericUpDown();
this.startAsyncButton = new System.Windows.Forms.Button();
this.cancelAsyncButton = new System.Windows.Forms.Button();
this.resultLabel = new System.Windows.Forms.Label();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
this.SuspendLayout();
//
// numericUpDown1
//
this.numericUpDown1.Location = new System.Drawing.Point(16, 16);
this.numericUpDown1.Maximum = new System.Decimal(new int[] {
91,
0,
0,
0});
this.numericUpDown1.Minimum = new System.Decimal(new int[] {
1,
0,
0,
0});
this.numericUpDown1.Name = "numericUpDown1";
this.numericUpDown1.Size = new System.Drawing.Size(80, 20);
this.numericUpDown1.TabIndex = 0;
this.numericUpDown1.Value = new System.Decimal(new int[] {
1,
0,
0,
0});
//
// startAsyncButton
//
this.startAsyncButton.Location = new System.Drawing.Point(16, 72);
this.startAsyncButton.Name = "startAsyncButton";
this.startAsyncButton.Size = new System.Drawing.Size(120, 23);
this.startAsyncButton.TabIndex = 1;
this.startAsyncButton.Text = "Start Async";
this.startAsyncButton.Click += new System.EventHandler(this.startAsyncButton_Click);
//
// cancelAsyncButton
//
this.cancelAsyncButton.Enabled = false;
this.cancelAsyncButton.Location = new System.Drawing.Point(153, 72);
this.cancelAsyncButton.Name = "cancelAsyncButton";
this.cancelAsyncButton.Size = new System.Drawing.Size(119, 23);
this.cancelAsyncButton.TabIndex = 2;
this.cancelAsyncButton.Text = "Cancel Async";
this.cancelAsyncButton.Click += new System.EventHandler(this.cancelAsyncButton_Click);
//
// resultLabel
//
this.resultLabel.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
this.resultLabel.Location = new System.Drawing.Point(112, 16);
this.resultLabel.Name = "resultLabel";
this.resultLabel.Size = new System.Drawing.Size(160, 23);
this.resultLabel.TabIndex = 3;
this.resultLabel.Text = "(no result)";
this.resultLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// progressBar1
//
this.progressBar1.Location = new System.Drawing.Point(18, 48);
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(256, 8);
this.progressBar1.Step = 2;
this.progressBar1.TabIndex = 4;
//
// backgroundWorker1
//
this.backgroundWorker1.WorkerReportsProgress = true;
this.backgroundWorker1.WorkerSupportsCancellation = true;
//
// FibonacciForm
//
this.ClientSize = new System.Drawing.Size(292, 118);
this.Controls.Add(this.progressBar1);
this.Controls.Add(this.resultLabel);
this.Controls.Add(this.cancelAsyncButton);
this.Controls.Add(this.startAsyncButton);
this.Controls.Add(this.numericUpDown1);
this.Name = "FibonacciForm";
this.Text = "Fibonacci Calculator";
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit();
this.ResumeLayout(false); }
#endregion [STAThread]
static void Main()
{
Application.Run(new FibonacciForm());
}
}
}
版本信息
 
 

.NET Framework

受以下版本支持:4、3.5、3.0、2.0

.NET Framework Client Profile

受以下版本支持:4、3.5 SP1

平台
 
 

Windows 7, Windows Vista SP1 或更高版本, Windows XP SP3, Windows XP SP2 x64 Edition, Windows Server 2008(不支持服务器核心), Windows Server 2008 R2(支持 SP1 或更高版本的服务器核心), Windows Server 2003 SP2

.NET Framework 并不是对每个平台的所有版本都提供支持。有关支持的版本的列表,请参见.NET Framework 系统要求

线程安全
 
 
此类型的任何公共 static(在 Visual Basic 中为 Shared) 成员都是线程安全的。但不保证所有实例成员都是线程安全的。
修订记录
 
 
 

Date

修订记录

原因

2010 年 9 月

已添加简单的 BackgroundWorker 代码示例。

客户反馈

c#的ThreadPool使用笔记(一)

http://www.cnblogs.com/sashow/archive/2007/02/08/644679.html

c#的ThreadPool使用笔记(二)

http://www.cnblogs.com/sashow/archive/2007/02/08/645016.html

C#多线程

http://blog.csdn.net/ILOVEMSDN/article/details/1735495

ThreadPool(线程池) in .Net

http://www.cnblogs.com/rickie/archive/2004/11/23/67275.html

上一篇:jmeter线程组介绍


下一篇:Java多线程之后台线程不执行finally