C#中关于Task(任务)的简单介绍

Task

通过ThreadPool.QueueUserWorkItem发起的异步线程存在局限性;

  • 异步计算完成后没有返回值
  • 主线程不知道异步计算何时完成

FCL提供了Task类型,提供了更加丰富计算限制的异步操作,全部是在线程池中创建的线程。

//以下两个函数效果等同与ThreadPool.QueueUserWorkItem
new Task(Dowork,paramter).Start();
Task.Run(()=>Dowork(paramter));

Task创建新的线程方式都提供了重载,可以传递一个CancellationToken实例。

等待任务完成

var task=new Task<bool>(Dowork);
task.Start();
task.Wait();//这里会引发当前线程的阻塞,当Dowork执行完成之后,才会继续执行下面的代码

Console.WriteLine($"Dowork执行结束了,结果是:"+task.Result)

如果计算限制的任务抛出未处理的异常,异常会被“吞噬”并存储到一个集合中,而线程可以返回到线程池中。调用Wait方法或者Result属性时,会抛出一个System.AggregateException对象

System.AggregateException类型封装了异常对象的一个集合(如果父任务生成了多个子任务,而多个子任务都抛出了异常,这个集合便可能包含多个异常)

Task类还提供了两个静态方法,允许线程等待一个Task对象数组:

WaitAny方法,阻塞调用线程,直到数组中的任何Task对象完成。返回数组索引,指明完成的是哪个Task对象。

WaitAll方法,阻塞调用线程,直到数组中所有Task对象完成。如果所有Task对象都完成,返回True;超时返回False.

WaitAny和WaitAll如果通过一个Cancellation取消,会抛出一个OperationCanceledException。

取消任务

用一个CancellationTokenSource取消Task,需要修改方法,让他接受一个CancellationToken

        private static int Sum(CancellationToken ct,  int n)
        {
            int sum = 0;
            for(;n>0;n--)
            {
                //ct.ThrowIfCancellationRequested();
                //任务有办法表示完成,任务甚至能返回一个值 所以需要采取一种方式将已完成的任务和出错的任务分开, 这就是一种方式
                if (ct.IsCancellationRequested) break;   //执行结果输出sum = 0
                checked { sum += n; }
            }
            return sum;
        }
        
                static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(state=>Console.WriteLine("123232323"));
            CancellationTokenSource cts = new CancellationTokenSource();

            Task<int> t = new Task<int>(n => Sum(cts.Token,(int)n),1000000);

            t.Start();

            cts.Cancel();
            
            try
            {
                Console.WriteLine("The sum is:" + t.Result);
            }
            catch (AggregateException x)
            {
                x.Handle(e => e is OperationCanceledException);
                Console.WriteLine("Sum is canceled");
            }
            Console.Read();
        }

如果任务被取消 ,ThrowIfCancellationRequested会抛出一个OperationCanceledException(选择抛出异常是为了区分已完成的任务和出错的任务)

任务完成时自动启动新任务

伸缩性好的软件不应该使得线程阻塞,调用wait,或者在任务尚未完成时查询任务的Result属性,不利于性能和伸缩性。

幸好,有更好的办法可以知道一个任务在什么时候结束运行:任务完成时可启用另一个任务

            Task<int> t = new Task<int>(n => Sum(cts.Token,(int)n),1000);

            Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is:" + t.Result));

如果在调用ContinueWith之前,任务已经完成,那么ContinueWith方法看到任务已经完成会立即启用显示结果的任务

Task对象内部包含了ContinueWith任务的一个集合,所以,实际可以用一个Task对象来多次调用ContinueWith。任务完成时,所有ContinueWith任务都会进入线程池的队列中。

任务可以启动子任务

任务支持父子关系

Task<int[]> parent = new Task<int[]>(() =>
{
    var results = new int[3];//创建一个数组来存储结果

     //这个任务创建并启动3个子任务
     new Task(() => results[0] = Sum(cts.Token, 1000), TaskCreationOptions.AttachedToParent).Start();
     new Task(() => results[1] = Sum(cts.Token, 2000), TaskCreationOptions.AttachedToParent).Start();
     new Task(() => results[2] = Sum(cts.Token, 3000), TaskCreationOptions.AttachedToParent).Start();

     //返回对数组的引用(即使数组元素可能还没有初始化)
     return results;
});

var cwt2 = parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));

parent.Start();

TaskCreationOptions.AttachedToParent标志将一个Task和创建他的Task关联,除非所有的子任务(以及子任务的子任务)结束运行,否则创建任务(父任务)不认为已经结束

一个任务创建的一个或多个Task对象默认是顶级任务,与创建它们的任务无关。

任务内部揭秘

每个Task对象都有一组字段,字段构成了任务的状态。包括ID、代表Task执行状态的Int32、对父任务的引用、对Task创建时指定的TaskSchedule的引用、对回调方法的引用等。

任务的创建是有代价的,必须为所有状态分配内存。如果不需要任务的附加功能,那么使用ThreadPool.QueueUserWorkItem能获得更好的资源利用率。

在一个Task对象的存在期间,可查询Task的只读Status属性了解它在生存期的什么位置

    public enum TaskStatus
    {
        //     该任务已初始化,但尚未被计划。
        Created = 0,
        //     该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。
        WaitingForActivation = 1,
        //     该任务已被计划执行,但尚未开始执行。 ContinueWith,ContinueWhenAll,ContinueWhenAny,FromAsync
        WaitingToRun = 2,
        //     该任务正在运行,但尚未完成。
        Running = 3,
        //     该任务已完成执行,正在隐式等待附加的子任务完成。
        WaitingForChildrenToComplete = 4,
        //     已成功完成执行的任务。
        RanToCompletion = 5,
        //     该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的
        //     CancellationToken 发出了信号。有关详细信息,请参阅任务取消。
        Canceled = 6,
        //     由于未处理异常的原因而完成的任务。
        Faulted = 7
    }

为了简化编码,Task提供了几个属性 IsCanceled,IsFaulted和IsCompleted

任务处于 RanToCompletion,Canceled,Faulted状态是,IsCompleted返回True。

判断一个任务是否成功完成最简单的方式是

if(task.Status == TaskStatus.RanToCompletion)

任务工厂

有时需要创建一组共享相同配置的Task对象。

为了避免重复的将相同的参数传给每个Task的构造器,可创建一个任务工厂来封装通用的配置。

System.Threading.Tasks 定义了

TaskFactory 用于创建返回void的任务

TaskFactory 用于创建返回特定返回类型的任务

        Task parent2 = new Task(() =>
        {
            var cts = new CancellationTokenSource();

            var tf = new TaskFactory<int>(
                cts.Token,
                TaskCreationOptions.AttachedToParent,
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default
            );

            var childTasks = new[]{
                tf.StartNew(()=> Sum(cts.Token,1000)),
                tf.StartNew(()=> Sum(cts.Token,2000)),
                tf.StartNew(()=> Sum(cts.Token,3000))
            };
        });

任务调度器

TaskSchedule对象负责执行被调度的任务,同时向Visual Studio调试器公开任务信息。FCL提供两个派生自TaskSchedule的类型:线程任务调度器和同步上下文调度器。默认情况下,所有应用程序使用的都是线程任务调度器。可以查询TaskSchedule的静态Default属性来获得对默认任务调度器的引用

同步上下文调度器适合提供了图形界面的应用程序,例如Windows窗体、WPF、Windows Store应用程序。它将所有的任务都调度给应用程序的GUI线程,使所有任务代码都能成功更新UI组件。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值