当前位置:编程学习 > asp >>

谈.Net委托与线程——创建无阻塞的异步调用(一)

 

前言

本文大部分内容来自于mikeperetz的Asynchronous Method Invocation及本人的一些个人体会所得,希望对你有所帮助。原英文文献可以在codeproject中搜索到。

介绍

这篇文章将介绍异步调用的实现机制及如何调用异步方法。大多数.NET开发者在经过delegate、Thread、AsynchronousInvocation之后,通常都会对以上概念产生混淆及误用。实际上,以上概念是.NET2.0版本中对并行编程的核心支持,基于概念上的错误认识有可能导致在实际的编程中,无法利用异步调用的特性优化我们的程序,例如大数据量加载引起的窗体”假死”。事实上这并不是一个困难的问题,该文将以一种逐层深入、抽丝剥茧的方式逐渐深入到异步编程的学习中。

同步与异步

大多数人并不喜欢阅读大量的文字说明,而喜欢直接阅读代码,因此,我们在下文中将主要以代码的形式阐述同步与异步的调用。

同步方法调用

假设我们有一个函数,它的功能是将当前线程挂起3秒钟。

	
		static void Sleep()
{
    Thread.Sleep(3000);
}

通常,当你的程序在调用Sleep后,它将等待3秒钟的时间,在这3秒钟时间内,你不能做任何其他操作。3秒之后,控制权被交回给调用线程(通常也就是你的主线程,即WinForm程序的UI线程)。这种类型的调用称为同步,本次调用顺序如下:

●  调用Sleep();

●  Sleep()执行中;

●  Sleep()执行完毕,控制权归还调用线程。

我们再次调用Sleep()函数,不同的是,我们要基于委托来完成这次调用。一般为了将函数绑定在委托中,我们要定义与函数返回类型、参数值完全一致的委托,这稍有点麻烦。但.NET内部已经为我们定义好了一些委托,例如MethodInvoker,这是一种无返回值、无参数的委托签名,这相当于你自定义了一种委托:

	
		public delegate void SimpleHandler();

执行以下代码:

	
		MethodInvoker invoker = new MethodInvoker(Sleep);
invoker.Invoke();

我们使用了委托,但依然是同步的方式。主线程仍然要等待3秒的挂起,然后得到响应。

注意:Delegate.Invoke是同步方式的。

异步方法调用

如何在调用Sleep()方法的同时,使主线程可以不必等待Sleep()的完成,一直能够得到相应呢?这很重要,它意味着在函数执行的同时,主线程依然是非阻塞状态。在后台服务类型的程序中,非阻塞的状态意味着该应用服务可以在等待一项任务的同时去接受另一项任务;在传统的WinForm程序中,意味着主线程(即UI线程)依然可以对用户的操作得到响应,避免了”假死”。我们继续调用Sleep()函数,但这次要引入BeginInvoke。

	
		MethodInvoker invoker = new MethodInvoker(Sleep);
invoker.BeginInvoke(null, null);

●  注意BeginInvoke这行代码,它会执行委托所调用的函数体。同时,调用BeginInvoke方法的线程(以下简称为调用线程)会立即得到响应,而不必等待Sleep()函数    的完成。

●  以上代码是异步的,调用线程完全可以在调用函数的同时处理其他工作,但是不足的是我们仍然不知道对于Sleep()函数的调用何时会结束,这是下文将要解决的问    题。

●  BeginInvoke可以以异步的方式完全取代Invoke,我们也不必担心函数包含参数的情况,下文介绍传值问题。

注意:Delegate.BeginInvoke是异步方式的。如果你要执行一项任务,但并不关心它何时完成,我们就可以使用BeginInvoke,它不会带来调用线程的阻塞。

对于异步调用,.NET内部究竟做了什么?

一旦你使用.NET完成了一次异步调用,它都需要一个线程来处理异步工作内容(以下简称异步线程),异步线程不可能是当前的调用线程,因为那样仍然会造成调用线程的阻塞,与同步无异。事实上,.NET会将所有的异步请求队列加入线程池,以线程池内的线程处理所有的异步请求。对于线程池似乎不必了解的过于深入,但我们仍需要关注以下几点内容:

●  Sleep()的异步调用会在一个单独的线程内执行,这个线程来自于.NET线程池。

●  .NET线程池默认包含25个线程,你可以改变这个值的上限,每次异步调用都会使用其中某个线程执行,但我们并不能控制具体使用哪一个线程。

●  线程池具备最大线程数目上限,一旦所有的线程都处于忙碌状态,那么新的异步调用将会被置于等待队列,直到线程池产生了新的可用线程,因此对于大量异步请      求,我们有必要关注请求数量,否则可能造成性能上的影响。

简单了解线程池

为了暴露线程池的上限,我们修改Sleep()函数,将线程挂起的时间延长至30s。在代码的运行输出结果中,我们需要关注以下内容:

●  线程池内的可用线程数量。

●  异步线程是否来自于线程池。

●  线程托管ID值。

上文已经提到,.NET线程池默认包含25个线程,因此我们连续调用30次异步方法,这样可以在第25次调用后,看看线程池内部究竟发生了什么。

	
		private void Sleep()
{
    int intAvailableThreads, intAvailableIoAsynThreds;

    //  取得线程池内的可用线程数目,我们只关心第一个参数即可
    ThreadPool.GetAvailableThreads(out intAvailableThreads,
            out intAvailableIoAsynThreds);

    //  线程信息
    string strMessage =
        String.Format("是否是线程池线程:{0},线程托管ID:{1},可用线程数:{2}",
            Thread.CurrentThread.IsThreadPoolThread.ToString(),
            Thread.CurrentThread.GetHashCode(),
            intAvailableThreads);

    Console.WriteLine(strMessage);

    Thread.Sleep(30000);
}

private void CallAsyncSleep30Times()
{
    //  创建包含Sleep函数的委托对象
    MethodInvoker invoker = new MethodInvoker(Sleep);

    for (int i = 0; i < 30; i++)
    {
        //  以异步的形式,调用Sleep函数30次
        invoker.BeginInvoke(null, null);
    }
}

输出结果:

对于输出结果,我们可以总结为以下内容:

●  所有的异步线程都来自于.NET线程池。

●  每次执行一次异步调用,便产生一个新的线程;同时可用线程数目减少。

●  在执行异步调用25次后,线程池中不再有空闲线程。此时,应用程序会等待空闲线程的产生。

●  一旦线程池内产生了空闲线程,它会立即被分配给异步任务等待队列,之后线程池中仍然不具备空闲线程,应用程序主线程进入挂起状态继续等待空闲线程,这样      的调用一直持续到异步调用被执行完30次。

针对以上结果,我们对于异步调用可以总结为以下内容:

●  每次异步调用都在新的线程中执行,这个线程来自于.NET线程池。

●  线程池有自己的执行上限,如果你想要执行多次耗费时间较长的异步调用,那么线程池有可能进入一种”线程饥饿”状态,去等待可用线程的产生。

BeginInvoke和EndInvoke

我们已经知道,如何在不阻塞调用线程的情况下执行一个异步调用,但我们无法得知异步调用的执行结果,及它何时执行完毕。为了解决以上问题,我们可以使用EndInvoke。EndInvoke在异步方法执行完成前,都会造成线程的阻塞。因此,在调用BeginInvoke之后调用EndInvoke,效果几乎完全等同于以阻塞模式执行你的函数(EndInvoke会使调用线程挂起,一直到异步函数执行完毕)。但是,.NET是如何将BeginInvoke和EndInvoke进行绑定呢?答案就是IAsyncResult。每次我们使用BeginInvoke,返回值都是IAsyncResult类型,它是.NET追踪异步调用的关键值。每次异步调用之后的结果如何?如果要了解具体执行结果,IAsyncResult便可视为一个标签。通过这个标签,你可以了解异步调用何时执行完毕,更重要的是,它可以保存异步调用的参数传值,解决异步函数上下文问题。

我们现在通过几个例子来了解IAsyncResult。如果之前对它了解不多,那么就需要耐心的将它领悟,因为这种类型的调用是.NET异步调用的关键内容。

	
		private void SleepOneSecond()
{
    //  当前线程挂起1秒
    Thread.Sleep(1000);
}

private void UsingEndInvoke()
{
    // 创建一个指向SleepOneSecond的委托
    MethodInvoker invoker = new MethodInvoker(SleepOneSecond);

    //  开始执行SleepOneSecond,但这次异步调用我们传递一些参数
    //  观察Delegate.BeginInvoke()的第二个参数
    IAsyncResult tag = invoker.BeginInvoke(null, "passing some state");

    // 应用程序在此处会造成阻塞,直到SleepOneSecond执行完成
    invoker.EndInvoke(tag);

    // EndInvoke执行完毕,取得之前传递的参数内容
    string strState = (string)tag.AsyncState;

    Console.WriteLine("EndInvoke的传递参数" + tag.As
补充:Web开发 , ASP.Net ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,