C# Task

Author Avatar
huuhghhgyg 3月 25, 2022
  • 在其它设备中阅读本文章

Task

线程的问题:线程(Thread)是用来创建并发的一种低级别工具,尤其在于以下方面有一些限制:

  • 虽然开始线程的时候可以方便地传入数据,但是当Join(等待)的时候,很难从线程获得返回值。
    • 可能需要设置一些共享字段
    • 如果抛出异常,捕获和传播该异常都很麻烦
  • 无法告诉线程在结束时开始做另外的工作,必须进行Join操作,在进程中阻塞当前的进程,等待线程结束。
  • 对手动同步的更大依赖以及随之而来的问题
    Task类可以很好地解决上述问题。Task是一个相对高级的抽象,它代表了一个并发操作(concurrent),该操作可能由Thread支持,或不由Thread支持。并且,Task是可组合的。
  • Tasks可以使用线程池来减少启动延迟
  • 使用TaskCompletionSource,Tasks可以利用回调的方式在等待I/O绑定操作是完全避免线程。

开始一个Task

开始一个Task最简单的方法就是使用Task.Run这个静态方法。(.NET4.5开始,.NET4.0的时候是Task.Factory.StartNew这个静态方法) 使用方法:传入一个Action委托即可。

Task默认使用线程池,也就是后台线程。当主线程结束时,创建的所有任务都会结束。

static void Main(string[] args)
{
    Task.Run(() => Console.WriteLine("Foo"));
    Console.ReadLine(); // 可以达到阻塞线程的效果。
    // 如果程序运行完了,由于Task是后台线程,Task也会被关闭。
}

Task.Run返回一个Task对象,可以使用它来监视其过程。Task.Run之后没有调用Start,因为该方法创建的是“热”任务(hot task);可以通过Task的构造函数创建“冷”任务(cold task),但很少这样做。

Wait

调用Task的Wait方法会进行阻塞直到操作完成(相当于调用Thread上的Join方法)

static void Main(string[] args)
{
    Task task = Task.Run(() =>
    {
        Thread.Sleep(3000);
        Console.WriteLine("Foo");
    }); //创建一个“热任务”

    Console.WriteLine(task.IsCompleted); //False

    task.Wait(); //阻塞至task完成操作

    Console.WriteLine(task.IsCompleted); //True

    // False
    // Foo
    // True
}

Wait也可以指定一个超时时间和取消令牌来提前结束等待。

长时间运行的任务

  • 默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Compute-Bound类工作
  • 针对长时间运行的任务或者阻塞操作(例如前面的例子),可以不采用线程池
  • 如果同时运行多个long-running tasks(尤其是其中有处于阻塞态的),那么性能将会受很大影响,这时有比TaskCreationOptions.LongRunning更好的办法:
    • 如果任务是IO-Bound,TaskCompletionSource和异步函数可以让你用回调代替线程实现并发。
    • 如果任务是Compute-Bound,生产者/消费者队列允许你对任务的并发性进行限流,避免把其它线程和进程饿死。(并行编程)

Task的返回值

Task有一个泛型子类叫做Task<TResult>,他允许发出一个返回值。

使用Func<TResult>委托或兼容的Lambda表达式来调用Task.Run就可以得到Task<TResult>,随后可以通过Result属性来获得返回的结果。

如果这个task还没有完成操作,访问Result属性会阻塞该线程直到该task完成操作。

static void Main(string[] args)
{
    Task<int> task = Task.Run(() =>
    {
        Console.WriteLIne("Foo");
        return 3;
    }); //Lambda表达式与Func<TResult>兼容

    int result = task.Result; //如果task没完成,会阻塞当前进程,直到返回结果
    Console.WriteLine(result); //3
}

Task<TResult>可以看作是一种所谓得“许诺”,在它里面包裹着一个Result,在稍后的时候就会变得可用。

Task的异常

不同于Thread,Task可以很方便地传播异常。如果task里面抛出了ige未处理的异常(故障),那么异常就会重新抛出给:

  • 调用了wait()的地方、
  • 访问了Task<TResult>Result属性的地方

无需重新抛出异常,通过Task的IsFaulted和IsCancelled属性也可以检测出Task是否发生了故障:

  • 如果两个属性都返回false,那么没有错误发生。
  • 如果IsCanceled位true,那就说明一个OperationCanceledException为该Task抛出了异常
  • 如果IsFaultedtrue,那就说明另一个类型的异常被抛出,而Exception属性也将指明错误

“自治”的Task

“自治”的Task制“设置完就不管了”的Task。指不通过调用Wait()方法、Result属性或continuation进行会合的任务。

针对自治的Task,需要像Thread一样,显式地处理异常,避免发生“悄无声息”的故障。
自治Task上未处理的异常称为未观察到的异常

未观察到的异常

可以通过全局的TaskScheduler.UnobservedTaskException来订阅未观察到的异常。

使用超时进行等待的Task,如果在超时后发生故障,那么它将会产生一个“未观察到的异常”

在Task发生故障后,如果访问Task的Exception属性,那么该异常就被认为是“已观察到的异常”

Continuation

当Task结束的时候,继续做其它事情。Continuation通常是通过回调的方式实现的,当操作一结束,就开始执行。

在Task上调用GetAwaiter会返回一个awaiter对象。它的OnCompleted方法会告诉task:“当你结束/发生故障时要执行委托”

可以将Continuation附加到已经结束的task上面,此时Continuation将会被安排立即执行。

Awaiter

awaiter是可以暴露下列两个方法和一个属性的对象:

  • OnCompleted
  • GetResult
  • IsCompleted(bool属性)
    其中,OnCompletedINotifyCompletion的一部分

如果发生故障,调用awaiter.GetResult()的时候,异常会重新抛出。
无需调用GetResult,task的Result属性也可以直接访问。

非泛型Task

非泛型的task,GetResult()方法有一个void返回值,只用来重新抛出异常。

未完…

async
标志着这些代码是异步的,编译器必须重新排列它。

link
本文链接:
发文时间
3月 25, 2022
请遵循协议