专栏首页逸鹏说道.NET中的异步编程上

.NET中的异步编程上

开篇

异步编程是程序设计的重点也是难点,还记得在刚开始接触.net的时候,看的是一本c#的Winform实例教程,上面大部分都是教我们如何使用Winform的控件以及操作数据库的实例,那时候做的基本都是数据库的demo,数据量也不大,程序在执行的时候基本上不会出现阻塞的情况。随着不断的深入.net,也开始进入的实战,在实际的项目,数据量往往都是比较大,特别是在大量的数据入库以及查询数据并进行计算的时候,程序的UI界面往往卡死在那里,发生了阻塞,这时候就需要对计算时间限制的过程进行异步处理,让UI线程继续相应用户的操作,使得用户体验表现比较友好,同时正确的使用异步编程去处理计算限制的操作和耗时IO操作还能提升的应用程序的吞吐量及性能。由此可见,异步编程的重要性。

异步编程在程序设计中也是非常复杂的,稍有不慎,就会使得你的应用程序变得不稳定,出现异常,甚至会奔溃。但是,比较幸运的是,.net提供非常方便的框架来进行异步编程,在我看来.net中实现异步有两种方式,第一种是多线程的方式,第二种是使用异步函数,其实在异步函数中使用的还是多线程的技术。接下来就介绍在.net中如何使用多线程和异步函数来解决计算限制、耗时等这些不友好用户体验的问题。

异步编程中比较关心,也是比较重要的技术点在于,1)当异步线程在工作完成时如何通知调用线程,2)当异步线程出现异常的时候该如何处理,3)异步线程工作的进度如何实时的通知调用线程。4)如何在调用线程中取消正在工作的异步线程,并进行回滚操作。

一、异步函数模型

c#中提供异步函数编程模式,只要是使用委托对象封装的函数都可以实现该函数的异步调用,这是因为委托类型有BeginInvokeEndInvoke这两个方法来支持异步调用。

下面给出一个例子来讲解如何使用委托的来实现异步调用函数。

class Program
    {        public delegate void DoWork();        static void Main(string[] args)
        {
            DoWork d = new DoWork(WorkPro);//no.1 
            d.BeginInvoke(null, null);//no.2
            for (int i = 0; i < 100; i++)//no.3            {
                Thread.Sleep(10);//主线程需要做的事            }
            Console.WriteLine("主线程done");
            Console.ReadKey();
        }        public static void WorkPro()
        {            //做一些耗时的工作
            Thread.Sleep(2000);
            Console.WriteLine("异步调用结束");
        }
    }

程序定义了一个DoWork类型无参无返回值的的委托类型,no.1用WorkPro方法实例化一个DoWork类型的对象d ,no.2通过委托对象dBeginInvoke(null,null)(下面将会详细介绍BeginInvoke函数中两个参数如何使用)来实现WorkPro函数的异步调用,这样就使得no.3主线程所做的for循环和WorkPro函数可以同时执行,这样使得程序的运行效率得到了大幅度的提升。如果程序是同步执行的话,假设WorkPro函数执行需要2秒,for需要1秒,总共执行时间就需要3秒,如果WorkPro是异步执行的话,那么整个程序执行完毕只需要2秒就够了。

------

上面这个例子只是简单演示了如何通过委托来实现函数的异步调用,而没有传递给该异步函数任何的参数,也不需要获取该异步函数的结果。如果主线需要传递给该异步函数一个参数,并且还要在该异步函数执行完毕之后获取其执行结果,那应该如何实现呢?

 class Program
    {        public delegate int DoWord(int count);        static void Main(string[] args)
        {
            DoWord d = new DoWord(WorkPro);
            IAsyncResult r= d.BeginInvoke(1000,null,null);//no.1
            int result= d.EndInvoke(r);//no.2            Console.WriteLine(result);            for (int i = 0; i < 100; i++)//no.3            {
                Thread.Sleep(10);//主线程需要做的事            }
            Console.WriteLine("主线程done");
            Console.ReadKey();
        }        public static int WorkPro(int count)
        {            int sum = 0;            //做一些耗时的工作
            for (int i = 0; i < count; i++)
            {
                sum += i;
            }            return sum;        
        }
    } 

我们已经把委托类型改为具有一个int类型的参数和int类型返回值。在这里解释一下,每当你的编译器发现定义了一个委托类型,就会对应的生成一个类型,并且该类型BeginInvoke方法的参数个数也是不同的,本例声明的委托类型为:

public delegate int DoWord(int count);

实际生成的BeginInvoke原型为:IAsyncResult BeginInvoke(int count, AsyncCallBack callback, object @object)

在no.1处还是和第一个例子一样调用委托,不同的是用IAsyncResult接口的变量接收了异步调用(并不是异步函数)的返回状态,这是方便后面调用EndInvoke方法接受这个异步函数调用结果而使用的,也可以通过该参数查看异步函数执行的状态,该接口有一个IsCompleted的属性。在no.2处使用d.EndInvoke(r)来接受异步函数返回值的。必须指出的是,主线程在调用委托的EndInvoke(r)方法时,当异步函数没有执行完毕的话,主线程会一直处于阻塞,等待异步函数执行完毕,获取返回值之后才执行no.3的for循环。这样就还会导致主线程处于阻塞状态。

理想的状态的是,当异步函数调用完成之后,自动通知任务执行完成。当然委托也能够做到,这就要使用BeginInvoke方法的后两个参数啦。看下面这个例子。

class Program
    {        public delegate int DoWord(int count);        static void Main(string[] args)
        {
            DoWord d = new DoWord(WorkPro);
            IAsyncResult r= d.BeginInvoke(100,CallBack ,d);//no.1
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);//主线程需要做的事            }
            Console.WriteLine("主线程done");
            Console.ReadKey();
        }        public static int WorkPro(int count)
        {            int sum = 0;            //做一些耗时的工作
            for (int i = 0; i < count; i++)
            {
                sum += i;
                Thread.Sleep(10);
            }            return sum;        
        } 
        public static void CallBack(IAsyncResult r)
        {
            DoWord d = (DoWord)r.AsyncState;
            Console.WriteLine("异步调用完成,返回结果为{0}", d.EndInvoke(r));
        }
    }

首先来解释一下BeginInvoke方法的第二个参数是AsyncCallBack 类型的委托(回调函数),当该参数不为空,那么在异步函数执行完毕之后,会调用该委托;第三个参数Object 类型的,代表传递给回调函数的异步调用状态。CallBack回调函数必须带有一个IAsyncResult 类型的参数,通过这个参数可以在回调方法内部获取异步调用的结果。在no.1出就给BeginInvoke函数传递了回调函数CallBack,和委托d,当异步数WorkPro执行完毕之后,就立即通知CallBack回调函数来显示执行结果。这下主线程就不需要阻塞一直的等待异步函数的结果,大大的提升了程序的运行效率。在.net还提供许多类的BeinXXX()EndXXX()的异步版本,比如文件的读写等,具体可以查阅相关的资料。

其中异步函数内部所使用的线程均是线程池中的工作线程,由线程池去分配管理的。

二、多线程模型

.net在System.ThreadingSystem.Threading.Tasks这两个命名空间中提供了ThreadThreadPool,和Task三个类来处理多线程的问题,其中Thread是建立一个专用线程,ThreadPool是使用线程池中工作线程,而Task类是采用任务的方式,其内部也是使用线程池中的工作线程。本节只讲Tread类和Tasks类的使用以及其优劣。

1、Thread类

Thread类的使用方法很简单,它开辟的是一个专用线程,不是线程池中的工作线程,不由线程池去管理。该类提供4个重载版本,常见的使用前面两个就好了。

1)public Thread( ThreadStart start ):其中ThreadStart是一个无参无返回值的委托类型。

2)public Thread( ParameterizedThreadStart start ):其中ParameterizedThreadStart 是一个带有一个Object类型的参数,无返回值的委托类型。

Thread类提供了两个构造函数可以看出,Thread类能够异步调用无参无返回值的函数,也能够异步调用带一个Object类型的无返回值的函数。下面就给出一个例子简单的演示一下如何使用Thread异步执行一个带参数的函数。

class Program
    {        static void Main(string[] args)
        {
            Thread t = new Thread(WorkPro);//no.1
            t.IsBackground = true;//no.2
            t.Start(1000);//no.3        }        public static void WorkPro(object  t)
        {            //做一些耗时的工作   
            int count=(int)t;            for (int i = 0; i < count; i++)
            {
                Thread.Sleep(2000);
            }
 
            Console.WriteLine("任务处理完成");
        }
    }

no.1实例化一个Thread对象,给传入一个ParameterizedThreadStart 类型的委托;no.2将建立的专用线程设置为后台的任务线程(后台线程会随着调用线程(即使任务没完成)的终止而强制终止,而前台线程如果任务没有处理完,是不会随着调用线程的终止而终止的);no.3调用Start(1000)方法,其中1000是传递给异步执行函数的参数。记住,如果构造Thread对象是ThreadStart委托,那么Start()就直接调用,否则会出现异常。只需要简单的几行代码就能实现函数的异步调用。

其中,当异步函数中处理需要多个参数时,那么只需要建立一个参数类,参数类中包括你函数需要的参数个数,然后将这个参数类传递给异步函数即可。

Thread类的使用虽然简单,但是它还是有一定的劣势的,一般不推荐使用。

1)Thread类创建的是一个专用线程,建立一个专用线程是非常耗用系统的资源,建议是使用线程池中的线程。

2)Thread类不能很好的和调用线程进行交互,当任务完成时不能及时的通知,在调用线程也不能随时的取消正在进行的任务。

另外在以下情况下,就只能选择使用Thread类了。

1)执行任务的线程要以非普通的优先级去执行,因为线程池的线程都是以普通优先级运行的。

2)执行任务的线程要表现为一个前台线程,因为线程池的线程始终都是一个后台线程。

3)异步执行的任务需要长时间的,那么就可以使用Thread类为该任务建立一个专用线程。

本文分享自微信公众号 - 我为Net狂(dotNetCrazy),作者:msay

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2016-03-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • C# 温故而知新: 线程篇(二) 上

    线程池和异步线程 目录: 1 什么是CLR线程池? 2 简单介绍下线程池各个优点的实现细节 3 线程池ThreadPool的常用方法介绍 4 简单理解下异步线程...

    逸鹏
  • C#线程篇---解答线程之惑(2)

    我们都知道,在这个行业,追求的就是用最少的时间学最多的知识,这是我写这个系列最想达到的目标,在最快的时间内,帮助更多的人学习更多的线程知识。 前一篇,...

    逸鹏
  • C#线程篇---线程池如何管理线程(6完结篇)

    C#线程基础在前几篇博文中都介绍了,现在最后来挖掘一下线程池的管理机制,也算为这个线程基础做个完结。   我们现在都知道了,线程池线程分为工作者线程和I/O线程...

    逸鹏
  • 如何在SpringBoot中异步请求和异步调用

    链接 | cnblogs.com/baixianlong/p/10661591.html

    一个优秀的废人
  • AsyncTask.cancel()的结束问题

    听着music睡
  • java多线程系列_线程的生命周期(4)

    与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。下面给出了Thread类中...

    Hongten
  • LockSupport实现线程挂起和唤醒——深入浅出原码分析

    如果只是LockSupport在使用起来比Object的wait/notify简单,那还真没必要专门讲解下LockSupport。最主要的是灵活性。

    须臾之余
  • [Java并发系列]Java并发工具类

    用户2017109
  • 【转】 Java 多线程之一

    进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括...

    shirayner
  • Java四种线程池的使用

    Java通过Executors提供四种线程池,分别为: newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲...

    Java架构师历程

扫码关注云+社区

领取腾讯云代金券