.NET中的异步编程下

时间:2022-05-07
本文章向大家介绍.NET中的异步编程下,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

2、Task类

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

三、小结

虽然在.net中提供了众多的异步编程模式,但是推荐最好使用Task类,因为Task类使用线程池中的任务线程,又由线程池管理,效率相对来说较高,而且Task类内部有比较好的机制,能让调用线程与任务进行交互。反正不管用哪种模式,总之尽量不要出现阻塞的情况,只要程序中出现线程阻塞,线程池就会创建新的活动线程,因为线程池总是要保证活动的任务线程数量与CPU的核数一致,它觉得这样性能最佳,当阻塞的线程恢复正常之后,线程池又会将多余的线程销毁,避免系统调度线程时频繁的进行上下文切换。这样的创建、销毁线程是非常的浪费系统资源影响性能的。而在线程同步的时候常常会出现阻塞的情况,所以能设计不用线程同步去解决问题,尽量不用线程同步。最后要是有写的不对的地方,请各位指正,谢谢!