C# 异步编程基础(五)Task

此入门教程是记录下方参考资料视频的过程
开发工具:Visual Studio 2019

参考资料:https://www.bilibili.com/video/BV1Zf4y117fs

目录

C# 异步编程基础(一)线程和阻塞

C# 异步编程基础(二)线程安全、向线程传递数据和异常处理

C# 异步编程基础(三)线程优先级、信号和线程池

C# 异步编程基础(四) 富客户端应用程序的线程 和 同步上下文 Synchronization Contexts

C# 异步编程基础(五)Task

C# 异步编程基础(六)Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay

C# 异步编程基础(七)异步原理

C# 异步编程基础(八) 异步函数

C# 异步编程基础(九) 异步中的同步上下文、ValueTask

C# 异步编程基础(十) 取消(cancellation)、进度报告、TAP(Task-Based Asynchronous Pattern)、Task组合器

线程(Thread)的问题

  1. 线程(Thread)是用来创建并发(concurrency)的一种低级别工具,它有一些限制,尤其是:
    虽然开始线程的时候可以方便的传入数据,但是当Join的时候,很难从线程获得返回值
       可能需要设置一些共享字段
       如果操作抛出异常,捕获和传播该异常都很麻烦
    无法告诉线程在结束时开始做另外的工作,你必须进行Join操作(在进程中阻塞当前的线程)
  2. 很难使用较小的并发(concurrent)来组件大型的并发
  3. 导致了对手动同步的更大依赖以及随之而来的问题

Task class

  1. Task类可以很好的解决上述问题
  2. Task说一个相对高级的抽象:它代表了一个并发操作(concurrent)
    该操作可能由Thread支持,或不由Thread支持
  3. Task是可组合的(可使用Continuation把它们串成链)
    Tasks可以使用线程池来减少启动延迟
    使用TaskCompletionSource,Tasks可以利用回调的方式,在等待I/O绑定操作时完全避免使用线程

开始一个Task Task.Run

  1. Task类在System.Threading.Tasks命名空间下
  2. 开始一个Task最简单的办法就是使用Task.Run(.NET 4.5,4.0的时候就是Task.Factory.StartNew)整个静态方法:
    传入一个Action委托即可

例子

static void Main(string[] args)
{
    Task.Run(()=>Console.WriteLine("Foo"));
}
  1. Task默认使用线程池,也就是后台线程:
    当主线程结束时,你创建的所有tasks都会结束(例子task)
  2. Task.Run返回一个Task对象,可以使用它来监视其过程
    在Task.Run之后,我们没有调用Start,因为该方法创建的是“热”任务(hot task)
       可以通过Task的构造函数创建“冷”任务(cold taks),但是很少这样做
  3. 可以通过Task的Status属性来跟踪task的执行状态

等待 Wait

  1. 调用task的Wait方法会进行阻塞直到操作完成
    相当于调用thread上的Join方法

例子

static void Main(string[] args)
{
    Task task=Task.Run(()=>{
        Thread.Sleep(3000);
        Console.WriteLine("Foo");
    });
    Console.WriteLine(task.IsCompleted);//fasle
    task.Wait();//阻塞直到task完成操作
    Console.WriteLine(task.IsCompleted);//true
}
  1. Wait也可以让你指定一个超时时间和一个取消令牌来提前结束等待,具体看 Help Viewer

长时间运行的任务 Long-running tasks

  1. 默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Compute-Bound类工作
  2. 针对长时间运行的任务或者阻塞操作(例如前面的例子),你可以不采用线程池

例子

static void Main(string[] args)
{
    Task task=Task.Factory.StartNew(()=>
    {
        Thread.Sleep(3000);
        System.Console.WriteLine("Foo");
    },TaskCreationOptions.LongRunning);
}
  1. 如果同时运行多个long-running tasks(尤其是其中有处于阻塞状态的),那么性能将会受很大影响,这时有比TaskCreationOptions.LongRunning更好的办法:
    如果任务是IO-Bound,TaskCompletionSource和异步函数可以让你用回调(Coninuations)代替线程来实现并发
    如果任务是Compute-Bound,生产者/消费者队列允许你对任务的并发性进行限流,避免把其它线程和进程饿死

Task的返回值

  1. Task有一个泛型子类叫做Task,它允许发出一个返回值
  2. 使用Func委托或兼容的Lambda表达式来调用Task.Run就可以得到Task
  3. 随后,可以通过Result属性来获得返回的结果
    如果这个task还没有完成操作,访问Result属性会阻塞线程直到该task完成操作

例子

static void Main(string[] args)
{
    Task<int> task=Task.Run(()=>
    {
        Console.WriteLine("Foo");
        return 3;
    });
    //如果task没完成,那么就阻塞
    int result=task.Result;
    //3
    Console.WriteLine(result);
}

例子,输出2到3000000中质数的个数

static void Main(string[] args)
{
    Task<int> primeNumerTask = Task.Run(() =>
          Enumerable.Range(2, 3000000).Count(n =>
               Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));

    Console.WriteLine("Task running");
    Console.WriteLine("The answer is " + primeNumerTask.Result);
}
  1. Task可以看作是一种所谓的“未来/许诺”(future、promise),在它里面包裹着一个Result,在稍后的时候就会变得可用
  2. 在CTP版本的时候,Task实际上叫做Future

Task的异常

  1. 与Thread不一样,Task可以很方便的传播异常
    如果你的task里面抛出了一个未处理的异常(故障),那么该异常就会重新抛出给:
       调用了Wait()的地方
       访问了Task的Result属性的地方

例子

static void Main(string[] args)
{
    Task task=Task.Run(()=>{throw null;});
    try
    {
        task.Wait();
    }
    catch(AggregateException aex)
    {
        if(aex.InnerException is NullReferenceException)
        {
            Console.WriteLine("Null");
        }
        else
        {
            throw; 
        }
    }
}
  1. CLR将异常包裹在AggregateException里,以便在并行编程场景中发挥很好的作用
  2. 无需重新抛出异常,通过Task的IsFaulted和IsCanceled属性也可以检测出Task是否发生了故障:
    如果两个属性都返回false,那么没有错误发生
    如果IsCanceled为true,那就说明一个OperationCanceledException为该Task抛出了
    如果IsFaulted为true,那就说明另一个类型的异常被抛出了,而Exception属性也将指明错误

异常与“自治”的Task

  1. 自治的,“设置完就不管了”的Task。就是指不通过调用Wait()方法、Result属性或continuation进行会合的任务
  2. 针对自治的Task,需要像Thread一样,显式的处理异常,避免发生“悄无声息的故障”
  3. 自治Task上未处理的异常称为未观察到的异常

未观察到的异常

  1. 可以通过全局的TaskScheduler.UnobservedTaskException来订阅未观察到的异常
  2. 关于什么是“未观察到的异常”,有一些细微的差别:
    使用超时进行等待的Task,如果在超时后发生故障,那么它将会产生一个“未观察到的异常”。
    在Tsk发生故障后,如果访问Task的Exception属性,那么该异常就被认为是“已观察到的”。

Task 结束

posted @ 2021-02-09 14:59  .NET好耶  阅读(1170)  评论(0编辑  收藏  举报