创建线程的一种简单方式是定义一个委托,通过BeginInvoke方法异步调用它:Delegate类支持异步调用方法BeginInvoke,委托被异步调用之后,在后台,Delegate类会创建一个执行任务的线程。通俗的说,委托异步调用的时候会新启动一个新线程来执行委托对象所指向的方法。委托使用线程池来完成异步任务。线程池详见本章后面的内容。
为了演示委托的异步特性,我们首先定义一个方法TakesAWhile,作为委托的实例方法,我们在这个方法中,调用Thread.Sleep()方法,让它需要一定的时间才能执行完毕:
static int TakesAWhile(int data, int ms)
{
Console.WriteLine("TakesAWhile started");
Thread.Sleep(ms);
Console.WriteLine("TakesAWhile completed");
return ++data;
}
要在委托中调用这个方法,我们还必须定义一个具有相同参数和返回类型的委托,如下面的TakesAWhileDelegate所示:
public delegateint TakesAWhileDelegate(int data,int ms);
下面通过投票、等待句柄、异步回调三种方式来展现委托的异步调用:
投票是检查委托是否完成了任务。
在上面已经说明了,委托的异步调用是通过Delegate提供的方法BeginInvoke()来实现的,现在介绍一下这个方法的参数和返回值:
(1)、参数
在该方法中,参数分两个部分,前面一部分的参数个数、类型、顺序都跟委托所限定的参数一致,这一部分参数主要是承上启下的,先接受实参,然后又作为实参传递给委托对象所指向的方法;第二部分的参数有两个,按照顺序,分别是AsyncCallback类型和Object类型,这两个参数的用法放在后面的章节来说明。
(2)、返回值
BeginInvoke()方法的返回值类型是IAsyncResult类型,在IAsyncResult中,有一个属性IsCompleted,这个属性可以监控委托的异步调用是否完成(委托所指向的方法是否执行完成),如果委托的异步调用已经完成,那么就返回true,否则返回false,异步操作还没开始的时候,这个值也为false。
在投票模式下面,就是通过IAsyncResult的IsCompleted属性,来监控委托的异步调用是否完成,只要委托没有完成其任务,程序的主线程就继续执行while循环:
static void Main(string[] args)
{
TakesAWhileDelegate d1 = TakesAWhile;
IAsyncResult ar = d1.BeginInvoke(1, 3000,null, null);
while (!ar.IsCompleted)
{
Console.Write(".");
Thread.Sleep(50);
}
int result = d1.EndInvoke(ar);
Console.WriteLine("result: {0}", result);
}
运行应用程序,可以看到主线程和委托线程同时运行,在委托线程执行完毕后,主线程就停止循环。上面的程序执行结果入下:
.TakesAWhile started
...................................................TakesAWhile completed
result: 2
或者
TakesAWhile started
...................................................TakesAWhile completed
.result: 2
或者
TakesAWhile started
...................................................TakesAWhile completed
result: 2
或者
.TakesAWhile started
...................................................TakesAWhile completed
.result: 2
每次的执行结果都可能不同,在我的电脑上测试出了四种结果,很显然,线程的调度顺序是不受我们程序控制的,是由操作系统来控制的。
我们在程序中,先通过IAsyncResult.IsCompleted属性监控子线程是否执行完成,然后结合while循环,来等待子线程的执行完成。除了这种方式之外,还可以在主线程中,调用委托类型的EndInvoke()方法来等待子线程的执行完成。
委托的EndInvoke()方法的参数是IAsyncResult,这个参数必须是同一个委托对象BeginInvoke方法的返回值;EndInvoke()方法返回同一个委托对象所调用的函数的返回值。
这个方法会阻塞主线程的执行,处于停顿状态,一直等待,直到同一个委托对象完成其任务为止。
如果不等待委托完成其任务就结束主线程,委托线程就会停止。
针对上面的委托异步调用,作如下说明:
(1)、关于主线程与子线程的优先级:启动的时候,肯定是主线程先启动,但是在执行的过程中,完全由操作系统去调度,控制优先级,以优先级别为准来调度。
(2)、在上面的执行中,执行环境都是在一个4核CPU上面,即主线程和子线程可能在一个核上执行,也可能在多个核上分别执行,但是有一个问题,新建子线程的时候,那么此时应该优先调度子线程还是继续执行主线程?此时主线程的时间片执行完成了吗?
WINDOWS平台下的线程是采用竞争机制执行的,所以新创建一个线程,并不能保证它马上就被执行,操作系统认为这个线程可以被执行后就会给它执行所需的资源,然后放在CPU的指令队列里,让它处于就绪状态,等它的优先级为最高时才分配给他CPU时间。所以没法确定哪个线程优先执行。即便是在单核上,也一样满足这个模式,一样由操作系统来调度,一样没法确认哪个线程优先执行。这个也是上面执行的结果出现了四种情况的原因。
主线程在子线程新建的时候,其时间片是不是刚好执行完了,这个也是根本就没法确定的。
很显然,多线程的调度完全是由操作系统来决定的,对于用户来说,是没法去控制的,在写程序的时候,一定要注意,各个线程的调用对用户来说是随机的,不可控的,所以一定要将各种可能的线程调度顺序都考虑进来,看看每种情况是否会有问题。
(3)、主线程和子线程Sleep的时间同时到达了,那么此时应该优先调度子线程还是继续执行主线程?
这个涉及线程从阻塞状态到就绪状态再到执行状态的过程。从理论上来说,一个线程sleep之后就处于阻塞状态了,当sleep完成之后,就应该获取资源处于就绪状态,然后再等待执行:一个线程的时间片执行完成之后,它就会释放资源处于阻塞状态,该线程何时能够再获取资源处于就绪状态、获取CPU的时间处于执行状态,这个也是由操作系统决定的。
(4)、一个CPU的一个核上面,一个时间片只能执行一个线程,但是如果是多核CPU或者是多个CPU,就能实现多个线程在同一个时间片上同时进行。一个CPU的一个核上面,线程的执行肯定会满足上面前三点所总结的规律,如果是多核CPU,那就更加让线程调度的不确定性增加,因为在同一个时间片上多个线程能够同时进行。
等待异步委托执行的另一种方式是使用与IAsyncResult相关的等待句柄。
在上面已经说明了,委托的异步调用是通过Delegate提供的方法BeginInvoke()来实现的,现在介绍一下这个方法的参数和返回值:
(1)、参数
参数在2.2.1.1小结中已经说明,在本节中没用到参数,就不作详细说明来。
(2)、返回值
BeginInvoke()方法的返回值类型是IAsyncResult类型,在IAsyncResult中,有一个AsyncWaitHandle属性,AsyncWaitHandle属性是WaitHandle类型的对象,这个对象有一个方法可以阻止当前线程的执行,来等待委托线程的执行,但是并不一定会等待委托线程的执行完成,这个要看调用的是哪个重载函数。具体实现等待的是AsyncWaitHandle属性的一个方法WaitOne,这个方法有很多的重载方法,在这里先介绍两个:
A、WaitOne(int millisecondsTimeout)
这个方法可以指定等待超时时间,这个方法的功能就是阻止当前线程的执行,等待同一个委托对象任务的完成,如果在指定的等待时间内没有完成,时间到就返回false,结束终止当前线程;如果等待时间大于委托任务执行完成所需要的时间,那么当委托任务执行完成之后,主线程立马返回true,并结束阻止当前线程的执行,而不会一直等到等待时间结束之后再反回true。如果millisecondsTimeout为-1,那么就永久的等待委托线程的执行,并阻止当前线程的执行,直到委托线程执行完毕,这个函数才会返回true,并结束阻止当前线程的执行。
B、WaitOne()
如果当前实例收到信号,则为 true。如果当前实例永远收不到信号,则 WaitOne 永不返回。这个跟上面的函数中,millisecondsTimeout为-1的情况是一样的。
下面的例子如果等待操作成功,就用一个中断退出while循环,用委托的EndInvoke()方法接收结果。
static void Main(string[] args)
{
TakesAWhileDelegate d1 = TakesAWhile;
IAsyncResult ar = d1.BeginInvoke(1, 3000,null, null);
while (true)
{
Console.Write(".");
if (ar.AsyncWaitHandle.WaitOne(50))
{
Console.WriteLine("Can get the result now");
break;
}
}
int result = d1.EndInvoke(ar);
Console.WriteLine("result: {0}", result);
Console.ReadLine();
}
提示:
等待句柄的内容详见本章后面的“同步”一节。
等待委托结果的第三种方式是使用异步回调。
在上面已经说明了,委托的异步调用是通过Delegate提供的方法BeginInvoke()来实现的,现在介绍一下这个方法的参数和返回值:
(1)、参数
参数在2.2.1.1小结中已经说明,它有两部分参数,前面那一部分的参数已经清楚了,第二部分的参数有两个,按照顺序,分别是AsyncCallback类型和Object类型,下面分别介绍这两个参数:
A、类型AsyncCallback是一个委托,AsyncCallback委托定义了一个IAsyncResult类型的参数,其返回类型是void。
B、类型Object可以接收任意对象,以便从回调方法中访问它。可以传送当前委托的实例,这样回调方法就可以使用它获得异步方法的结果。
(2)、BeginInvoke方法回调
当异步委托执行完毕之后,会马上回调AsyncCallback所指向的方法,并将BeginInvoke的返回值IAsyncResult对象ar作为参数传递给AsyncCallback所指向的方法,在该方法中,又可以通过ar的 AsyncState属性获取BeginInvoke方法的最后一个参数。
现在,先定义一个基于委托AsyncCallback的方法TakesAWhileCompleted,然后把这个方法传递给BeginInvoke方法的AsyncCallback参数。
然后将异步委托d1传递给BeginInvoke方法的Object参数。
只要委托TakesAWhileDelegate的对象d1完成了其异步调用,就立马调用TakesAWhileCompleted方法,不需要在主线程中等待结果,该方法的参数来源于d1的返回值IAsyncResult对象ar,在该方法中,通过ar的AsyncState属性可以获取BeginInvoke()方法的最后一个参数d1。
static void Main(string[] args)
{
TakesAWhileDelegate d1 = TakesAWhile;
d1.BeginInvoke(1, 3000, TakesAWhileCompleted, d1);
for (int i = 0; i < 100; i++)
{
Console.Write(".");
Thread.Sleep(50);
}
Console.ReadLine();
}
在以下方法中,可以通过调用d1的EndInvoke()方法获得d1委托异步调用的结果。
static void TakesAWhileCompleted(IAsyncResult ar)
{
if (ar == null) throw newArgumentNullException("ar");
TakesAWhileDelegate d1 = ar.AsyncStateas TakesAWhileDelegate;
Trace.Assert(d1 != null, "Invalid object type");
int result = d1.EndInvoke(ar);
Console.WriteLine("result: {0}", result);
}
使用回调方法,必须注意这个方法在委托线程中调用,而不是在主线程中调用。但是在委托线程的任务未完成之前,不能停止主线程,因为主线程停止后子线程也一定会停止,哪怕子线程还没有执行完。
回调方法虽然是在委托异步调用完成之后才被调用的,但是经过测试,回调函数跟委托异步调用的线程是同一个,并非新起了一个线程。