.NET中的异步编程

开篇html

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

 
异步编程在程序设计中也是很是复杂的,稍有不慎,就会使得你的应用程序变得不稳定,出现异常,甚至会奔溃。可是,比较幸运的是,.net提供很是方便的框架来进行异步编程,在我看来.net中实现异步有两种方式,第一种是多线程的方式,第二种是使用异步函数,其实在异步函数中使用的仍是多线程的技术。接下来就介绍在.net中如何使用多线程和异步函数来解决计算限制、耗时等这些不友好用户体验的问题。
 
异步编程中比较关心,也是比较重要的技术点在于,1)当异步线程在工做完成时如何通知调用线程,2)当异步线程出现异常的时候该如何处理,3)异步线程工做的进度如何实时的通知调用线程。4)如何在调用线程中取消正在工做的异步线程,并进行回滚操做。
 
1、异步函数模型
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()的异步版本,好比文件的读写等,具体能够查阅相关的资料。
其中异步函数内部所使用的线程均是线程池中的工做线程,由线程池去分配管理的。
 
2、多线程模型

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


一、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类为该任务创建一个专用线程。
 

二、Task类c#

Task类是封装的一个任务类,内部使用的是ThreadPool类,提供了内建机制,让你知道何时异步完成以及如何获取异步执行的结果,而且还能取消异步执行的任务。下面看一个例子是如何使用Task类来执行异步操做的。
 class Program
    {
        static void Main(string[] args)
        {
            Task t = new Task((c) =>
                {
                    int count = (int)c;
                    for (int i = 0; i < count; i++)
                    {
                        Thread.Sleep(10);
                    }
                    Console.WriteLine("任务处理完成");
                }, 100);//no.1
            t.Start();
 
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
    }
no.1处使用Task的构造函数为:
public Task( Action<Object> action, Object state )一个 Action<Object>类型的委托(即异步调用函数具备一个 Object类型的参数),和一个 Object类型的参数,也就是传递给异步函数的参数,Task类还有几种方式的重载,咱们还能够传递一些 TaskCreationOptions标志来控制 Task的执行方式。在这里我使用的是 lambda表达去写委托的,这样使得程序的结构更加的清晰,使用 Start()来启动异步函数的调用。
--------
若是须要异步函数有返回值,那么此时就须要使用Task<TResult>泛型类(派生自Task)来实现,其中TResult表明返回的类型。由于异步函数具备返回值,因此Task<TResult>的各类重载版本的构造函数第一个委托类型的参数都是Fun<TResult>或者Fun<Object,TResult>。下面演示等待任务完成并获取其结果。
 class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = new Task<int>((c) =>
                {
                    int count = (int)c;
                    int sum=0;
                    for (int i = 0; i < count; i++)
                    {
                        Thread.Sleep(10);
                        sum+=i;
                    }
                    Console.WriteLine("任务处理完成");
                    return sum;
                }, 100);
            t.Start();
            t.Wait();//no.1
            Console.WriteLine("任务执行的结果{0}", t.Result);//no.2
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
}
若是任务中出现了异常,那么异常会被吞噬掉,并存储到一个集合中去,而线程能够返回到线程池中去。可是若是在代码中调用了Wait方法或者是Result属性,任务有异常发生就会被引起,不会被吞噬掉。其中Result属性内部自己也调用了Wati方法。Wait方法和上一节中的委托的EndInvoke方法相似,会使得调用线程阻塞直到异步任务完成。下面咱们会介绍如何避免获取异步结果的阻塞状况,在讲解以前,先说一下,如何取消正在运行的任务。
------
看下面一段代码如何演示取消正在运行的任务。
class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();//no.1
            Task<int> t = new Task<int>((c) =>Sum(cts.Token ,(int)c), 100);//no.2
            t.Start();
            cts.Cancel();//no.3若是任务还没完成,可是Task有可能完成啦
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
        static int Sum(CancellationToken ct, int count)
        {
            int sum = 0;
            for (int i = 0; i < count; i++)
            {
                if (!ct.CanBeCanceled)
                {
                    Thread.Sleep(10);
                    sum += i;
                }
                else
                {
                    Console.WriteLine("任务取消");
                    //进行回滚操做
                    return -1;//退出任务
                }
            }
            Console.WriteLine("任务处理完成");
            return sum;
        }
    }
取消任务要引用一个CancellationTokenSource 对象。在须要异步执行的方法中增长一个CancellationToken类型的形参。而后在异步函数的for循环代码中用一个if语句判断CancellationTokenCanBeCanceled属性,这个属性能够用来判断在调用线程是否取消任务的执行,除CanBeCanceled属性以外,还可使用ThrowIfCancellationRequested方法,该方法的做用是若是在调用线程调用CancellationTokenSource对象的Cancel方法,那么就会引起一个异常,而后在调用线程进行捕捉就行了,这是在异步函数中的处理方式。no.1在构建任务以前须要创建一个CancellationTokenSource ,no2.而且把CancellationTokenSource传递给异步调用函数,传递的是CancellationTokenSource对象的Toke属性,该属性是一个CancellationToken类型的对象。这样就完成任务的取消模式,若是想在调用线程中取消任务的执行, 只须要调用 CancellationTokenSource Cancel方法就行啦。
 
------
前面就说过了,获取任务结果调用Wait方法和Result属性致使调用线程阻塞,那么如何处理这种状况呢,这就使用了Task<TResult>类提供的ContinueWith方法。该方法的做用是当任务完成时,启动一个新的任务,不只仅是如此,该方法还有能够在任务只出现异常或者取消等状况的时候才执行,只须要给该方法传递TaskContinuationOptions枚举类型就能够了。下面就演示一下如何使用 ContinueWith方法。
首先看下ContinueWith方法的原型。
public  Task ContinueWith( Action<Task> continuationAction )采用一个Action<Task>类型的委托。该方法提供了多种重载的版本,这只是最简单的一种。
 

public Task ContinueWith( Action<Task> continuationAction, TaskContinuationOptions continuationOptions )第二个参数表明新任务的执行条件,当任务知足这个枚举条件才执行 Action<Task>类型的回调函数。安全

代码以下:
class Program
    {
        static void Main(string[] args)
        {           
            Task<int> t = new Task<int>((c) =>Sum((int)c), 100);
            t.Start();
            t.ContinueWith(task => Console.WriteLine("任务完成的结果{0}", task.Result));//当任务执行完以后执行
            t.ContinueWith(task => Console.WriteLine(""), TaskContinuationOptions.OnlyOnFaulted);//当任务出现异常时才执行
            for (int i = 0; i < 200; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
        static int Sum( int count)
        {
            int sum = 0;
            for (int i = 0; i < count; i++)
            {       
                    Thread.Sleep(10);
                    sum += i;          
            }
            Console.WriteLine("任务处理完成");
            return sum;
        }
    }
t.Start()以后调用第一个ContinueWith方法,该方法第一参数就是一个Action<Task>的委托类型,至关因而一个回调函数,在这里我也用lambda表达式,当任务完成就会启用一个新任务去执行这个回调函数。而第二个ContinueWith里面的回调方法却不会执行,由于咱们的任务也就是Sum方法不会发生异常,不能知足TaskContinuationOptions.OnlyOnFaulted这个枚举条件这种用法比委托的异步函数编程看起来要简单些。最关键的是ContinueWith的还有一个重载版本能够带一个TaskScheduler对象参数,该对象负责执行被调度的任务。FCL中提供两种任务调度器,均派生自TaskScheduler类型:线程池调度器,和同步上下文任务调用器。而在Winform窗体程序设计中TaskScheduler尤其有用,为何这么说呢?由于在窗体程序中的控件都是有ui线程去建立,而咱们所执行的后台任务使用线程都是线程池中的工做线程,因此当咱们的任务完成以后须要反馈到Winform控件上,可是控件建立的线程和任务执行的线程不是同一个线程,若是在任务线程中去更新控件就会致使控件对象安全问题会出现异常。因此操做控件,就必需要使用ui线程去操做。所以在ContinueWith获取任务执行的结果的并反馈到控件的任务调度上不能使用线程池任务调用器,而要使用同步上下文任务调度器去调度,即采用ui这个线程去调用ContinueWith方法所绑定的回调用函数即Action<Task>类型的委托。下面将使用任务调度器来把异步执行的Sum计算结果反馈到Winform界面的TextBox控件中。
界面以下。

代码以下。
 public partial class Form1 : Form
    {
        private readonly TaskScheduler contextTaskScheduler;//声明一个任务调度器
        public Form1()
        {
            InitializeComponent();
            contextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();//no.1得到一个上下文任务调度器
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            Task<int> t = new Task<int>((n) => Sum((int)n),100);
            t.Start();
            t.ContinueWith(task =>this.textBox1 .Text =task.Result.ToString(),contextTaskScheduler);//当任务执行完以后执行
            t.ContinueWith(task=>MessageBox .Show ("任务出现异常"),CancellationToken.None ,TaskContinuationOptions.OnlyOnFaulted,contextTaskScheduler );//当任务出现异常时才执行
        }
        int Sum(int count)
        {
            int sum = 0;
            for (int i = 0; i < count; i++)
            {
                Thread.Sleep(10);
                sum += i;
            }
            Console.WriteLine("任务处理完成");
            return sum;
        }
    }
在no.1窗体的构造函数获取该UI线程的同步上下文调度器。在按钮的事件接受异步执行的结果时候,都传递了contextTaskScheduler同步上下文的调度器,目的是,当异步任务完成以后,调度UI线程去执行任务完成以后的回调函数。
------
到目前为止,我日常用到的异步编程模式也就这么多了,固然Task类的ContinueWith还有不少重载的版本,会提供不同效果。在开篇的时候就说,如何在调用线程中实时获取异步任务的执行状况,好比个人任务是插入100w条数据到数据库,我在界面中须要实时的刷新数据导入的进度条,这种状况使用上述所讲的是作不到的。具体如何作到,我在另一篇文章已经详细的讲过啦,采用回调函数的方法(委托)来实现,连接: http://www.cnblogs.com/mingjiatang/p/5079632.html
 
3、小结
虽然在.net中提供了众多的异步编程模式,可是推荐最好使用Task类,由于Task类使用线程池中的任务线程,又由线程池管理,效率相对来讲较高,并且Task类内部有比较好的机制,能让调用线程与任务进行交互。反正无论用哪一种模式,总之尽可能不要出现阻塞的状况,只要程序中出现线程阻塞,线程池就会建立新的活动线程,由于线程池老是要保证活动的任务线程数量与CPU的核数一致,它以为这样性能最佳,当阻塞的线程恢复正常以后,线程池又会将多余的线程销毁,避免系统调度线程时频繁的进行上下文切换。这样的建立、销毁线程是很是的浪费系统资源影响性能的。而在线程同步的时候经常会出现阻塞的状况,因此能设计不用线程同步去解决问题,尽可能不用线程同步。最后要是有写的不对的地方,请各位指正,谢谢!
相关文章
相关标签/搜索