欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-05-08 08:00:55
前言 在.Net程序开发过程中,我们经常会遇到如下场景: 编写WinForm程序客户端,需要查询数据库获取数据,于是我们根据需求写好了代码后,点击查询,发现界面卡死,无法响应。经过调试,发现查询数据库这一步执行了很久,在此过程中,UI被阻塞,无法响应任何操作。 如何解决此问题?我们需要分析问题成因: ......

前言

在.net程序开发过程中,我们经常会遇到如下场景:

编写winform程序客户端,需要查询数据库获取数据,于是我们根据需求写好了代码后,点击查询,发现界面卡死,无法响应。经过调试,发现查询数据库这一步执行了很久,在此过程中,ui被阻塞,无法响应任何操作。

如何解决此问题?我们需要分析问题成因:在winform窗体运行时,只有一个主线程,即为ui线程,ui线程在此过程中既负责渲染界面,又负责查询数据,因此在大量耗时的操作中,ui线程无法及时响应导致出现问题。此时我们需要将耗时操作放入异步操作,使主线程继续响应用户的操作,这样可以大大提升用户体验。

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

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

第一个异步编程模型:apm

概述

apm,全称asynchronous programing model,顾名思义,它即为异步编程模型,最早出现于.net framework 1.x中。

它使用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获取结果,那么会发生什么呢?

如下图所示:

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

看到这里大家也许会比较诧异:为什么同步操作会在异步操作之后输出呢?这样不是和同步就一样了吗?

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

同时在begininvoke返回的iaynscresult中,包含如下属性:

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

通过轮询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);
        }
    }
}

运行后结果如下:

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

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

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

在之前的操作中,使用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);
        }
    }
}

运行后结果如下:

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

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

小结

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

同时我们可以看到,begininvoke方法,实际上是调用了线程池中的线程进行操作,因此apm模型也应属于多线程程序,同时包含主线程与线程池线程。

但是apm模型也存在一些缺点:

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

2、在异步操作的过程中,无法取消,也无法得知操作进度。

3、若编写gui程序,异步操作内容与主线程未在同一线程,操作控件时会引起线程安全问题。

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

下集预告

浅谈.net异步编程的前世今生----eap篇