22.1 进程、线程以及异步编程
当我们启动一个程序时,系统在内在中创建一个新的进程(process)。进程就是一组资源,它们构成了一个正在运行的程序。这些资源包括虚拟地址空间、文件句柄以及程序启动需要的其他东西的载体。
在进程中,系统创建了一个叫线程(thread)的内核对象,线程体现了一个程序的真实执行情况。(线程是执行线程的缩写。)一旦程序准备完毕,系统在线程中开始执行Main方法的第一条语句。
有关线程的一些重要事项如下:
- 默认情况下,一个进程只包含了一个线程,它从程序开始执行一直到程序结束。
- 一个线程可以发起另一个线程,因此,在任何时候,一个进程都可能在不同状态有多个线程来执行程序的不同部分。
- 如果一个进程中有多个线程,它们会分享进程的资源。
- 纯种是由系统负责调度的处理器(而不是进程)上的执行单元。
到目前为止,我们在书中使用的所有示例程序都只有一个线程,并且从程序的第一行语句到最后一行语句顺序执行, 这叫做同步编程。异步编程指的是程序发起多个线程, 它们在理论上是在同一时间执行的。(其实不一定真的在同一时间执行。)
如果程序运行在一个多处理器的系统上,不同的线程实际上可能在不同的处理器上同时执行。这可以大幅度提高性能,由于多核处理器的普及,我们需要写程序来利用这个机会。
然而,在单处理器的系统上,同一时间内处理器肯定只能执行一个指令。这种情况下,操作系统就会协调线程,使处理器在它们之间分享。每一个线程都可以获取称为时间片(time slice)的一小段时间,这种轮流分享处理器的方式使所有线程通过代码按照它们自己的方式进行工作。
多线程处理带来的问题
在程序中使用多个线程叫做多线程处理,它增加了程序的负荷和额外的复杂度,例如:
- 创建和销毁线程都有时间和资源的成本。
- 安排线程,把它们加载到处理器中以及在每次时间片段之后保存状态都需要时间。
- 由于进程中的线程共享相同的资源和堆,这就需要增加额外的编程复杂度来确保它们之间的工作不相干扰。
- 调试多线程程序非常困难,因为程序每次运行的时间不同可能就会产生不同的结果,所以在调试器中运行程序就是在浪费时间。
除这些问题外,多线程的好处相对这些成本来说是值得的——只要它被合理利用而不是被过度使用。例如,我们已经知道,在多处理器的系统上,如果不同的线程运行在不同的处理器上,执行效率会高很多。
为了帮我们降低创建和销毁线程相关的成本,CLR为每一个进程维护了一个线程池(thread pool)。开始进程的线程池是空的,如果进程使用的线程被创建,并且完成了线程的执行,它不会被销毁,而是加入到进程的线程池中。之后,如果进程需要另外一个线程,CLR就会从池中还原一个线程,这就节省了很多时间。
另外一个常见的示例是,多线程处理对GUI编程来说很重要。用户希望在他点击了按钮或使用键盘之后能有一个快速的响应,如果程序需要执行耗费一段时间的操作,就可以把它放在另外一个线程中进行,保持主线程用于对用户的输入进行反馈。如果程序在那段时间中没有响应,这是完全不可接受的。
多线程处理的复杂度
尽管从概念上说多线程处理不是很复杂,但是要让一个复杂程序在所有细节上都很完美是比较困难的,需要在如下方面进行考虑。
口线程之间的通信:有很多内建的机制能用于线程之间通信,因此通常只要使用内存就能完成了,对于同一个进程的所有线程来说,内存是可见的也是可访问的。
口协调线程:尽管创建线程很简单,我们还需要能协调它们的行为。例如, 一个线程可能需要在继续执行之前等待另外一个线程结束。
口同步资源使用:由于进程内的所有线程共享相同的资源和内存,我们需要确保不同的线程没有在同一时间访问和改变它们引起冲突的状态。
System.Threading命名空间包含的一些类和类型使我们可以创建复杂的多线程程序。包括Thread类本身以及诸如Mutex、Semaphore和Monitor等用来同步资源使用的类。
然而,我们可以通过两种简单的技术为程序增加非常强大的多线程处理 异步委托和计时器,在本章剩余部分会介绍这些内容。对于大多数程序来说,它们可能是我们需要的最好的技术。
22.2 异步编程模式
C#可以有一个简单易用的机制用于异步执行方法,那就是使用委托。
在第15章中, 我们介绍了委托的主题,并且了解到当委托对象调用时,它调用了它的调用列表中包含的方法。就像程序调用方法一样,这是同步完成的。
如果委托对象在调用列表中只有一个方法(之后会叫做引用方法),它就可以异步执行这个方法。委托类有两个方法,叫做Beginlnvoke和Endlnvoke,它们就是用来这么做的。这些方法以如下方式使用:
口当我们调用委托的Beglnlnvoke方法时,它开始在线程池中的独立线程上执行引用方法, 并且立即返回原始线程。原始线程可以继续,而引用方法会在线程池的线程中并行执行。
口当程序希望获取已完成的异步方法的结果时,可以检查Beginlnvoke返回的IAsyncResult 的IsCompleted属性,或调用委托的Endlnvoke方法来等待委托完成。
图22-1演示了使用这三种标准模式的过程。对于这三种模式来说,原始线程都发起了一个异步方法,然后做一些其他处理。然而,这些模式不同的是,原始线程获取发起的线程已经完成的消息的方式。
口在等待一直到完成(wait-until-done)模式中,在发起了异步方法以及做了一些其他处理 之后,原始线程就中断并且等异步方法完成之后再继续。
口在轮询( polling)模式中,原始线程定期检查发起的线程是否完成,如果没有则可以继续做一些其他的事情。
口在回调(callback)模式中,原始线程一直执行,无需等待或检查发起的线程是否完成。
在发起的线程中的引用方法完成之后,发起的线程就会调用回调方法,由回调方法在调用Endlnvoke之前处理异步方法的结构。
22.3 BeginInvoke和EndInvoke
在学习异步编程模式的示例之前,让我们先研究一下Beginlnvoke和Endlnvoke方法。一些需要了解的有关Beginlnvoke的重要事项如下。
口在调用Beginlnvoke时,参数列表中的实参组成如下:
-引用方法需要的参数;
-两个额外的参数——callback参数和state参数。
口Beginlnvoke从线程池中获取一个线程并且在新的线程开始时运行引用方法。
口Beginlnvoke返回给调用线程一个实现IAsyncResult接口的对象。这个接口引用包含了异 步方法的当前状态,原始线程然后可以继续执行。
口之后的行声明了一个叫做del的Mydel委托类型的委托对象,并且使用Sum方法来初始化它的调用列表。
口最后一行代码调用了委托对象的Beginlnvoke方法并且提供了两个委托参数3和5,以及两 个Beginlnvoke的参数callback和state,在本例中都设为null。执行后,Beginlnfoke方法进行两个操作:
- 从线程池中获取一个线程并且在新的线程上开始运行Sum方法,提供它3和5作为参数。
- 它收集新线程的状态信息并且把IAsyncResult接口的引用返回给调用线程来提供这些数据。调用线程把它保存在一个叫做iar的变量中。
using System;
using System.Collections.Generic;
namespace ConsoleApplication5
{
class Class1
{
delegate long MyDel(int first, int second); //委托声明
static long Sum(int x, int y) //方法匹配委托
{
return x + y;
}
static void main()
{
MyDel del = new MyDel(Sum); //创建委托对象
IAsyncResult iar = del.BeginInvoke(3, 5, null, null);
//IAsyncResult 有关新线程的消息,del.BeginInvoke异步调用。 3,5为委托参数,null,null为额外参数。
}
}
}
EndInvoke方法用来获取由异步方法调用返回的值,并且释放线程使用的资源。Endlnvoke有如下的特性。
口它接受一个由BeglnInvoke方法返回的IAsyncResult对象的引用,并找到它关联的线程。
口如果线程池的线程已经退出,EndInvoke做如下的事情:
- 它清理退出线程的状态并且释放它的资源。
- 它找到引用方法返回的值并且把它的值作为返回值。
口如果当Endlnvoke被调用时线程池的线程仍然在运行,调用线程就会停止并等待,直到清 理完毕并返回值。因为Endlnvoke是为开启的线程进行清理,所以必须确保对每一个Beglnlnvoke都调用Endlnvoke。
口如果异步方法触发了异常,在调用EndInvoke时会抛出异常。
如下的代码行给出了一个调用EndInvoke并从异步方法获取值的示例。我们必须把IAsyncResult对象的引用作为参数。
long result = del.EndInvoke(iar); //long result 异步方法的返回值 del 委托对象 iar IAsyncResult对象
EndInvoke提供了从异步方法调用的所有输出,包括ref和out参数。如果委托的引用方法有ref或out参数,它们必须包含在EndInvoke的参数列表中,并且在IAsyncResult对象引用之前,如下所示。
等待—直到结束模式
在这种模式里,原始线程发起一个异步方法的调用,做一些其他处理,然后停止并等待,直到开启的线程结束。它总结下下:
IAsyncResult iar = del.BeginInvoke(3,5,null,null);
//Do additional work in the calling thread,while the method
//is being executed asynchronously in the spawned thread.
.....
long result = del.EndInvoke(iar);
如下代码给出了一个使用这种模式的完整示例。代码使用Thread类的Sleep方法将它自己挂起0.1秒。Thread类在System.Threading命名空间下。
using System;
using System.Threading;
namespace ConsoleApplication5
{
delegate long MyDel(int first, int second); //委托声明
class Class1
{
static long Sum(int x, int y) //方法匹配委托
{
System.Console.WriteLine(" Inside Sum");
Thread.Sleep(100);
return x + y;
}
static void main()
{
MyDel del = new MyDel(Sum); //创建委托对象
System.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);
}
}
}
AsyncResult类
既然我们已经在看到了BeginInvoke和EndInvoke的最简单形式,是时候来进一步接触IASyncResult了。它是使用这些方法的必要部分。
BeginInvoke返回一个IASyncResult接口的引用(内部是ASyncResult类的对象)。ASyncResult类表现了异步方法的状态。图22-2演示了这个类中的一些重要部分。有关这个类的重要事项如下:
- 当我们调用委托对象的BeginInvoke方法时,系统创建一个AsyncResult类的对象。然而,它不返回类对象的引用,而是返回对象中包含的IAsyncResult接口的引用。
- AsyncResult对象包含一个叫做AsyncDelegate的属性,它返回一个指向被调用来开启异步方法的委托的引用。但是,这个属性是类对象的一部分而不是接口的一部分。
- IsCompleted属性返回一个布尔值,表示异步方法是否完成。
- AsyncState属性返回一个对象的引用,它被作为BeginInvoke方法调用时的state参数。它返回object类型的引用。
轮询模式
在轮询模式中,原始纯种发起了异步方法的调用,做一些其他事情,然后使用IASyncResult对象的IsComplete属性来定期检查开启的线程是否完成。如果异步方法已经完成,原始纯种就调用EndInvoke并继续。否则,它做一些其他处理,然后再检查。这里的“处理”仅仅是由0数到10000000。
using System;
using System.Threading;
namespace ConsoleApplication5
{
delegate long MyDel(int first, int second); //委托声明
class Class1
{
static long Sum(int x, int y) //方法匹配委托
{
System.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");
long result = del.EndInvoke(iar);
Console.WriteLine("After EndInvoke:{0}",result);
}
}
}
回调模式
在之前的等待—直到结束模式以及轮询模式中,初始纯种继续它自己的控制流程,直到它知道开启的线程完成。然后,它获取结果并继续。
回调模式的不同之处在于,一旦初始经珞发起了异步方法,它就自己管自己了,不再考虑同步。当异步方法调用结束之后,系统调用一个用户自定义的方法来处理结果,并且调用委托的EndInvoke方法。这个用户自定义的方法叫做回调方法或回调。
BeginInvoke的参数列表中最后的两个额外参数被架设方法用作:
- 第一个参数,callback参数,是回调方法的名字。
- 第二个参数,state参数,可以是null或要传入回调方法的一个对象的引用。我们可以通过使用IAsyncResult参数的AsyncState属性来获取这个对象,参数的类型是object。
1.回调方法
回调方法的签名和返回类型必须和AsyncCallback委托类型所描述的形式一致。它需要方法接受一个IAsyncResult作为参数并且返回类型是void,如下所示: void AsyncCallback(IAsyncResult iar)
我们有多种方式可以为BeginInvoke方法提供回调方法。由于BeginInvoke中的callback参数是AsyncCallback类型的委托,我们可以以委托形式提供,如下面的第一行代码所示。或者,我们也可以只提供回调方法名称,让编译器为我们创建委托,两种形式是完全等价的。
//使用回调方法创建委托
IAsyncResult iar2 = del.BeginInvoke(3,5,CallWhenDoine,null); //只需要用回调方法的名字。
第二个BeginInvoke的参数是发送给回调方法的对象。它可以是任何类型的对象,但是参数类型是object,所以在回调方法中,我们必须转换成正确的类型。
2.在回调方法内调用EndInvoke
在回调方法内,我们的代码应该调用委托的EndInvoke方法来处理异步执行后的输出值。要调用委托的EndInvoke方法,我们肯定需要委托对象的引用,而它在初始线程中,不在开启的线程中。
如果我们不使用BeginInvoke的state参数作其他用途,可以使用它发送委托的引用给回调方法,如下所示:
IAsyncResult iar = del.BeginInvoke(3,5,CallWhenDone,del);
否则,我们可以从发送给方法作为参数的IAsyncResult对象中提取出委托的引用。如图22-3以及下面的代码所示:
- 给回调方法的参数只有一个, 就是刚结束的异步方法的IAsyncResult接口的引用。请记住,IAsyncResult接口对象在AsyncResult类对象的内部。
- 尽管IAsyncResult接口没有委托对象的引用,而包含它的AsyncResult类对象却有委托对象的引用。所以,示例代码方法体的第一行就通过转换接口引用为类类型来获取类对象的引用。就能量ar现在就有类对象的引用。
- 有了类对象的引用,我们现在就可以调用类对象的AsyncDelegate属性并且把它转化为合适的委托类型。这样就得到了委托引用,我们可以用它来调用EndInvoke。
using System.Runtime.Remoting.Messaging;using System;
namespace ConsoleApplication5
{
class class1
{
void CallWhenDone(IAsyncResult iar)
{
AsyncResult ar = (AsyncResult)iar; //获取类对象的引用。
MyDel del = (MyDel)ar.AsyncDelegate;
long Sum = del.EndInvoke(iar);
}
}
}
如下代码把所有知识点放在一起,给出了一个使用回调模式的示例。
using System.Runtime.Remoting.Messaging;
using System;
using System.Threading;
namespace Exam1
{
delegate long MyDel(int first,int second);
class class1
{
static long Sum(int x, int y)
{
System.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("Ding more work in Main.");
Thread.Sleep(500);
Console.WriteLine("Done with Main.Exiting.");
}
}
}
22.4 计时器
计时器提供了另外一种正规的重复运行异步方法的方法。尽管在.NET BCL中有好几个可用的Timer类,但在这里我们只会介绍System.Threading命名空间中的一个。
有关计时器需要了解的重要事项如下所示:
- 计时器在每次时间到期之后调用回调方法。回调方法必须是TimerCallBack委托形式的,结构如下所示。它接受了一个object类型作为参数,并且返回类型是void。
void TimeCallback(object state)
- 当计时器到期之后,系统会从线程池中的线程上开启一个回调方法,提供state对象作为其参数,并且开始运行。
- 我们可以设置的计时器的一些特性如下:
- dueTime是回调方法首次被调用之前的时间。如果dueTime被设置为特殊的值TimeOut.Infinite,则计时器不会开始,如果被设置为0,回调函数会被立即调用。
- period是两次成功调用回调函数之间的时间间隔。如果它的值设置为Timeout.Infinite,回调在首次被调用之后不会再被调用。
- state可以是null或在每次回调方法执行时要传入的对象的引用。
Timer类的构造函数接受回调方法名称、DueTime、period以及state作为参数。Timer有很多构造函数,最为常用的形式如下:
Timer(TimerCallback callback,object state,unit dueTime,unit period)
如下代码语句展示了一个创建Timer对象的示例:
Timer myTimer = new Timer(MyCallback,someObject,2000,1000);
一旦Timer对象被创建,我们可以使用Change方法来改变它的dueTime或period方法。
如下代码给出了一个使用计时器的示例。Main方法创建了一个计时器,2秒钟之后它会首次调用回调,然后每隔1秒再调用1次。回调方法只是输出了包含它被调用的次数的消息。
using System;
using System.Threading;
namespace Timers
{
class Class3
{
int TimesCalled = 0;
void Display(object state)
{
Console.WriteLine("{0} {1}",(string)state,++TimesCalled);
}
static void Main()
{
Class3 c = new Class3();
Timer myTimer = new Timer(p.Display,"Processing time event",2000,1000);
Console.WriteLine("Timer started.");
Console.ReadLine();
}
}
}
.NET BCL还提供了几个其他计时器类,每一个都有其用途。其他计时器如下所示:
- System.Windows.Forms.Timer:这个类在Windows应用程序中使用,用来定期把WM_TIMER消息放到程序的消息队列中。当程序从队列获取消息后,它会在主用户接口线程中异步处理,这对Windows应用程序来说非常重要。
- System.Timers.Timer:这个类更复杂,它包含了很多成员,使我们可以通过属性和方法来操作计时器。它还有一个叫做Elapsed的成员事件,每次时间到期就会发起这个事件。这个计时器可以运行在用户接口线程或工作者线程上。