<原>理解c#中的异步方法

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(2000true))
	{
		//做主线程接下来的工作。
	}
	//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委托。理解清楚这些方法,类,接口已经这几个之间的关系。就能够掌握异步调用的精髓了。

Leave a Reply