c#中构建多线程应用程序,不一定要通过System.Threading命名空间下的类来实现,通过delegate委托也能够实现多线程编程所需要的方式,(其实delegate就是利用CLR的ThreadPool来实现的,这个不再我们的讨论范围)。
先来说一下委托吧,通过当我们在应用程序中定义了一个委托后,其实在被编译成CIL后,就是一个类,我们可以通过ildasm.exe 这个程序来查看生成的CIL。
我们举个例子:
delegate int del (string str1, string str2)
del被编译后,生成的类的定义如下:
sealed class del: System.MulticastDelegate { public del(object target,uint functionAddress); public void Invoke(string str1,string str2); public IAsyncResult BeginInvoke(string str1,string str2,AsyncCallback cb ,object state); public int EndInvoke(IAsyncResult result); }
生成的Invoke()方法用来调用被代理对象同步方式维护的方法,调用线程(应用程序的主线程)会一直等待,直到委托调用完成。在c#中,Invoke()不会被显式的调用,而是在同步方式下自动被触发。
下面我们来说说BeginInvoke方法和EndInvoke()方法,这两个方法是在利用委托来实现异步调用的关键。
在本例中BeginInvoke方法如下:
IAsyncResult BeginInvoke(string str1,string str2, AsyncCallback cb ,object state);
可以看到BeginInvoke方法返回IAsyncResult,参数列表中含有四个参数,前两个参数如同del委托中参数的定义,后两个参数System.AsyncCallback和System.Object类型的。
在本例中的EndInvoke方法如下:
int EndInvoke(IAsyncResult result)
EndInvoke方法返回int ,参数列表中含有一个参数,参数类型为IAsyncResult。在BeginInvoke方法和EndInvoke方法中,BeginInvoke方法中除了最后两个参数,前面的参数必须和之前定义的委托中的参数一样;EndInvoke方法的返回值必须和之前定义的委托的返回值一样。
借助上面定义的委托,我们写这样一个函数
int stradd (string str1,string str2) { //仅仅为了测试一下异步方法的线程和调用线程是否是一致的。 Console.WriteLine("stradd thread id : ",Thread.CurrentThread.GetHashCode()); return str1.length+str2.length; }
下面定义一个主函数测试一下:
static void Main(string[] args) { Console.writeLine("Main thread id :",Thread.CurrentThread.GetHashCode()); del dd = new del(stradd); IAsyncResult result = dd.BeginInvoke("hello ","The another thread",null,null); //EndInvoke方法阻塞调用线程,直到stradd方法运行结束 int len = dd.EndInvoke(result); Console.WriteLine("the length of str1 + str2 ={0}",len); }
运行后我们可以知道异步调用就是创建了一个次线程。
通过上面的例子,我们可以看到一个异步调用的过程,但是,这样的调用对于多线程的要求,是毫无用处的,
endInvoke方法仍旧会阻塞主线程的运行。那么有没有好的方式来实现呢?我们想一下,如果能够在次线程还没有结束的时候,我们能够查看到次线程的运行的状态,在次线程还没有结束的时候,我们可以做主线程剩下的工作,这样的话,就不会让主线程一直在那里等待。
为了实现这样的机制,我们先来看一个接口IAsyncResult,这个接口在异步方法中很常见,回顾一下刚才看到得东西,BeginInvoke方法的返回值
EndInvoke方法的参数,都是这个接口。我们看一下这个接口的定义:
public interface IAsyncResult { object AsyncState{get ;} WaitHandle AsyncWaitHandle{get;} bool CompletedSynchronously{get;} bool IsCompleted{get;} }
IAsyncResult提供了IsCompleted接口,使用这个属性,调用线程可以在调用EndInvoke方法之前,自动判断异步调用是否完成。如果完成,返回true;
未完成返回false;
static void Main(string[] args) { Console.writeLine("Main thread id :",Thread.CurrentThread.GetHashCode()); del dd = new del(stradd); IAsyncResult result = dd.BeginInvoke("hello ","The another thread",null,null); while (result.IsCompleted) { //做主线程接下来的工作。 } //EndInvoke方法阻塞调用线程,直到stradd方法运行结束 int len = dd.EndInvoke(result); Console.WriteLine("the length of str1 + str2 ={0}",len); }
通过这样的一种轮询机制,我们能够实现调用线程不会被阻塞,同时能保证第一时间获取次线程运行的结果,但仔细想过之后就会发现这种方式的笨拙,要不断的执行
while循环中的内容,直到次线程完成。基于这一原因,IAsyncResult提供了AsyncWaitHandle属性实现更加灵活的控制。AsyncWaitHandle返回一个WaitHandle类型的实例
该实例含有一个WaitOne的公共方法,查看msdn,我们看到,这个方法可以阻塞调用线程,直到WaitHandle收到信号。
更改上面的main方法,如下所示:
static void Main(string[] args) { Console.writeLine("Main thread id :",Thread.CurrentThread.GetHashCode()); del dd = new del(stradd); IAsyncResult result = dd.BeginInvoke("hello ","The another thread",null,null); while (!result. AsyncWaitHandle.WaitOne(2000,true)) { //做主线程接下来的工作。 } //EndInvoke方法阻塞调用线程,直到stradd方法运行结束 int len = dd.EndInvoke(result); Console.WriteLine("the length of str1 + str2 ={0}",len); }
使用WaitHandle.waitOne(2000,true),指定最长等待时间为秒,秒后如果AsyncWaitHandle未受到次线程结束的信号,返回false;否则返回true。
通过上述两种方式,我们发现一个共同点,就是调用线程(主线程)在询问次线程是否完成了,并且这种询问是一次又一次的进行,这种方式显的笨拙。如果当次线程结束的时候
能够让他自己来通知主线程,说明自己结束了,这样的话,主线程就没有必要在花费时间来询问次线程,这样的方式显然更好理解。
想要实现这样的一种方式,我们就必须来理解BeginInvoke的后两个参数,我们先来介绍AsyncCallback参数,这个参数是一个委托,默认值是null。只要提供了AsyncCallback对象
当异步调用完成的时候,就会自动的调用AsyncCallback委托所指定的方法。AsyncCallback委托能够调用那些符合特定模式的方法,方法的签名如下所示:
void AsyncCallbackMethod(IAsyncResult iac);
我们还用上面的例子,这次用次线程结束后调用AsyncCallback委托指定的方法这样的方式来实现.
static void Main(string[] args) { Console.writeLine("Main thread id :",Thread.CurrentThread.GetHashCode()); del dd = new del(stradd); IAsyncResult result = dd.BeginInvoke("hello ","The another thread",new AsyncCallback(complete),null); //做主线程接下来的工作 } void complete (IAsyncResult iac) { Console.WriteLine("The stradd method has completed。");//告诉了主线程,次线程已经工作结束了。 }
在这个例子中,会发现我们在主线程中,由于没有调用EndInvoke方法(调用这个方法,会阻塞主线程,那么就等于没有体现异步的优点) 我们无法得到stradd方法运算的结果。现在注意complete方法的参数IAsyncResult iac。(又一次碰到了这个接口,可见他是多么的重要)
这是一个接口,我们没法直接使用它,在c#中存在一个实现了IAsyncResult接口的类AsyncResult。利用这个类,我们能够得到想要的结果。
下面我们修改complete方法,如下所示:
修改后,我们彻底完成了通过次线程通知主线程,来告知自己结束,这样一种异步调用的机制。
看到这里,我们发现,在BeginInvoke中,最后一个参数我们始终没有提及,这个参数的类型是System.Object的,所以可以传入任意想要看到的类型。
该参数允许从主线程传递额外的状态信息给AsyncCallback委托中的回调方法。
我们定义个额外的类
class Message
{
public string msg = "一些额外的内容";
}
修改main方法如下:
static void Main(string[] args)
{
Console.writeLine("Main thread id :",Thread.CurrentThread.GetHashCode());
del dd = new del(stradd);
IAsyncResult result = dd.BeginInvoke("hello ","The another thread",new AsyncCallback(complete),new Message());
//做主线程接下来的工作
Console.ReadLine();
}
修改complete方法如下:
void complete (IAsyncResult iac)
{
//为了在complete方法中获取获取BeginInvoke最后一个参数,使用IAsyncResult参数的AsyncState属性.
Message m = (Message)iac.AsyncState;
Console.WriteLine(m.msg);
Console.WriteLine("The stradd method has completed。");//告诉了主线程,次线程已经工作结束了。
}
Well done ! 这样我们就完成了对异步调用的讨论,下面我们总结一下,在异步调用中,我们需要深入理解委托的BeginInvoke方法,EndInvoke方法,IAsyncResult接口
AsyncResult类,AsyncCallback委托。理解清楚这些方法,类,接口已经这几个之间的关系。就能够掌握异步调用的精髓了。