浅谈.Net异步编程的前世此生----APM篇

前言数据库

在.Net程序开发过程当中,咱们常常会遇到以下场景:编程

编写WinForm程序客户端,须要查询数据库获取数据,因而咱们根据需求写好了代码后,点击查询,发现界面卡死,没法响应。通过调试,发现查询数据库这一步执行了好久,在此过程当中,UI被阻塞,没法响应任何操做。设计模式

如何解决此问题?咱们须要分析问题成因:在WinForm窗体运行时,只有一个主线程,即为UI线程,UI线程在此过程当中既负责渲染界面,又负责查询数据,所以在大量耗时的操做中,UI线程没法及时响应致使出现问题。此时咱们须要将耗时操做放入异步操做,使主线程继续响应用户的操做,这样能够大大提高用户体验。安全

直接编写异步编程也许不是一件轻松的事,和同步编程不一样的是,异步代码并非始终按照写好的步骤执行,且如何在异步执行完通知前序步骤也是其中一个问题,所以会带来一系列的考验。多线程

幸运的是,在.Net Framework中,提供了多种异步编程模型以及相关的API,这些模型的存在使得编写异步程序变得容易上手。随着Framework的不断升级,相应的模型也在不断改进,下面咱们一块儿来回顾一下.Net异步编程的前世此生。异步

第一个异步编程模型:APM异步编程

概述函数

APM,全称Asynchronous Programing Model,顾名思义,它即为异步编程模型,最先出现于.Net Framework 1.x中。spa

它使用IAsyncResult设计模式的异步操做,通常由BeginOperationName和EndOperationName两个方法实现,这两个方法分别用于开始和结束异步操做,例如FileStream类中提供了BeginRead和EndRead来对文件进行异步字节读取操做。线程

使用

在程序运行过程当中,直接调用BeginOperationName后,会将所包含的方法放入异步操做,并返回一个IAsyncResult结果,同时异步操做在另一个线程中执行。

每次在调用BeginOperationName方法后,还应调用EndOperationName方法,来获取异步执行的结果,下面咱们一块儿来看一个示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace APMTest
{
    class Program
    {
        public delegate void ConsoleDelegate();

        static void Main(string[] args)
        {
            ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
            Thread.CurrentThread.Name = "主线程Thread";
            IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
            consoleDelegate.EndInvoke(ar);
            Console.WriteLine("我是同步输出,个人名字是:" + Thread.CurrentThread.Name);
            Console.Read();
        }

        public static void ConsoleToUI()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            {
                Thread.CurrentThread.Name = "线程池Thread";
            }
            else
            {
                Thread.CurrentThread.Name = "普通Thread";
            }
            Thread.Sleep(3000); //模拟耗时操做
            Console.WriteLine("我是异步输出,个人名字是:" + Thread.CurrentThread.Name);
        }
    }
}

在这段示例中,咱们定义了一个委托来使用其BeginInvoke/EndInvoke方法用于咱们自定义方法的异步执行,同时将线程名称打印出来,用于区分主线程与异步线程。

如代码中所示,在调用BeginInvoke以后,当即调用了EndInvoke获取结果,那么会发生什么呢?

以下图所示:

看到这里你们也许会比较诧异:为何同步操做会在异步操做以后输出呢?这样不是和同步就同样了吗?

缘由是这样的:EndInvoke方法会阻塞调用线程,直到异步调用结束,因为咱们在异步操做中模拟了3s耗时操做,因此它会一直等待到3s结束后输出异步信息,此时才完成了异步操做,进而进行下一步的同步操做。

同时在BeginInvoke返回的IAynscResult中,包含以下属性:

经过轮询IsCompleted属性或使用AsyncWaitHandle属性,都可以获取异步操做是否完成,从而进行下一步操做,相关代码以下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace APMTest
{
    class Program
    {
        public delegate void ConsoleDelegate();

        static void Main(string[] args)
        {
            ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
            Thread.CurrentThread.Name = "主线程Thread";
            IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
            //此处改成了轮询IsCompleted属性,AsyncWaitHandle属性同理
            while (!ar.IsCompleted)
            {
                Console.WriteLine("等待执行...");
            }
            consoleDelegate.EndInvoke(ar);
            Console.WriteLine("我是同步输出,个人名字是:" + Thread.CurrentThread.Name);
            Console.Read();
        }

        public static void ConsoleToUI()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            {
                Thread.CurrentThread.Name = "线程池Thread";
            }
            else
            {
                Thread.CurrentThread.Name = "普通Thread";
            }
            Thread.Sleep(3000); //模拟耗时操做
            Console.WriteLine("我是异步输出,个人名字是:" + Thread.CurrentThread.Name);
        }
    }
}

运行后结果以下:

能够发现,在轮询属性时,程序仍然会等待异步操做完成,进而进行下一步的同步输出,没法达到咱们须要的效果,那么究竟有没有办法解决呢?

此时咱们须要引入一个新方法:使用回调。

在以前的操做中,使用BeginInvoke方法,两个参数总传入的为null。若要使用回调机制,则需传入一个类型为AsyncCallback的回调函数,并在最后一个参数中,传入须要使用的参数,如如下代码所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace APMTest
{
    class Program
    {
        public delegate void ConsoleDelegate();

        static void Main(string[] args)
        {
            ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
            Thread.CurrentThread.Name = "主线程Thread";
            //此处传入AsyncCallback类型的回调函数,并传入须要使用的参数
            consoleDelegate.BeginInvoke(CallBack, consoleDelegate);
            //IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
            ////此处改成了轮询IsCompleted属性,AsyncWaitHandle属性同理
            //while (!ar.IsCompleted)
            //{
            //    Console.WriteLine("等待执行...");
            //}
            //consoleDelegate.EndInvoke(ar);
            Console.WriteLine("我是同步输出,个人名字是:" + Thread.CurrentThread.Name);
            Console.Read();
        }

        public static void ConsoleToUI()
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            {
                Thread.CurrentThread.Name = "线程池Thread";
            }
            else
            {
                Thread.CurrentThread.Name = "普通Thread";
            }
            Thread.Sleep(3000); //模拟耗时操做
            Console.WriteLine("我是异步输出,个人名字是:" + Thread.CurrentThread.Name);
        }

        public static void CallBack(IAsyncResult ar)
        {
            //使用IAsyncResult的AsyncState获取BeginInvoke中的参数,并用于执行EndInvoke
            ConsoleDelegate callBackDelegate = ar.AsyncState as ConsoleDelegate;
            callBackDelegate.EndInvoke(ar);
        }
    }
}

运行后结果以下:

此时能够看出,使用回调的方式已经实现了咱们须要的效果。在同步执行时,将耗时操做放入异步操做,从而不影响同步操做的继续执行,在异步操做完成后,回调返回相应的结果。

小结

APM模型的引入,使得编写异步程序变的如此简单,只需定义委托,将要执行的方法包含其中,并调用Begin/End方法对,便可实现异步编程。在一些基础类库中,也已经提供了异步操做的方法,直接调用便可。

同时咱们能够看到,BeginInvoke方法,其实是调用了线程池中的线程进行操做,所以APM模型也应属于多线程程序,同时包含主线程与线程池线程。

可是APM模型也存在一些缺点:

一、若不使用回调机制,则需等待异步操做完成后才能继续执行,此时未达到异步操做的效果。

二、在异步操做的过程当中,没法取消,也没法得知操做进度。

三、若编写GUI程序,异步操做内容与主线程未在同一线程,操做控件时会引发线程安全问题。

为了解决这些缺陷,微软推出了其余的异步模式,预知后事如何,且听下回分解。

下集预告

浅谈.Net异步编程的前世此生----EAP篇

相关文章
相关标签/搜索