C#异步编程学习笔记2 之 Task

C#异步编程学习笔记2 之 Task

    • Task
      • Thread的问题
      • Task Class
        • 开始一个Task(Task.Run)
        • Wait 等待
        • 长时间运行的任务(Long-running tasks)
        • Task 的返回值
        • Task 的异常
          • 异常与“自治”的Task
          • 未观察到的异常
      • Continuation
        • awaiter
          • 如果发生故障
          • 非泛型task
        • 同步上下文
        • ContinueWith
      • TaskCompletionSource
        • 使用
        • TaskCompletionSource的真正魔力
        • Task.Delay

Task

Thread的问题

线程(Thread)时用来创建并发(concurrency)的一种低级别工具,它有一些限制,尤其是:

  • 虽然开始线程的时候方便传入数据,但是当 Join 的时候,很难从线程获得返回值。
    • 如果需要返回值,可能需要设置一些共享字段
    • 如果操作抛出异常,捕获和传播异常都很麻烦
  • 无法告诉线程在结束的时候开始做另外的工作,必须进行 Join 操作(在进程中阻塞当前线程)

所以很难使用较小的并发(concurrent)来组建大型的并发,导致了对手动同步的更大依赖以及随之而来的问题。

而 Task 类可以很好的解决上述问题。

Task Class

Task 是一个相对高级的抽象,它代表了一个并发操作(concurrent),该操作可能由 Thread 支持,或不由 Thread 支持。

  • Task 是可组合的(可使用 Continuation 把它们串成链
  • Tasks 可以使用线程池来减少启动延迟
  • 使用 TaskCompletionSource ,Tasks 可以利用回调的方式,在等待 I/O 的绑定操作时完全避免线程
开始一个Task(Task.Run)

Task 类在 System.Threading.Tasks 命名空间下

开始一个 Task 最简单的办法就是使用 Task.Run(.Net 4.5 添加的,.Net 4.0 的使用是 Task.Factory.StartNew)这个静态方法:

  • 使用时传入一个 Action 委托即可

    public static void Main(string[] args)
    {
        Task.Run(() => Console.WriteLine("Foo"));
    }
    
  • Task 默认使用线程池,也就是后台线程。当主线程结束时,创建的所有的 tasks 都会结束。可通过阻塞主线程的方法,来使创建的 tasks 完成执行。

    public static void Main(string[] args)
    {
        Task.Run(() => Console.WriteLine("Foo"));
        Console.ReadLine();
    }
    
  • Task.Run 返回一个 Task 对象,可以使用它来监视其过程。

  • 在 Task.Run 之后,没有调用 Start 方法,因为通过 Task.Run 创建的使“热”任务(host task),即创建完之后就准备开始运行,不需要调用 Start 方法。

    • 可以通过 Task 的构造函数来创建“冷”任务(cold task),但是很少这样做。
  • 可以通过 Task 的 Status 属性来跟踪 task 的执行状态。

Wait 等待

调用 task 的 Wait 方法会进行阻塞直到操作完成,相当于调用 Thread 的 Join 方法。

public 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
}

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

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

默认情况下,CLR 在线程池中运行 Task,这非常适合短时间运行的 Compute-Bound 类工作。

针对长时间运行的任务或者阻塞操作,可以不采用线程池的方法。

public static void Main(string[] args)
{
    Task task = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(3000);
                System.Console.WriteLine("Foo");
            }, TaskCreationOptions.LongRunning);
}

如果同时运行多个 long-running tasks(尤其是其中由处于阻塞状态的),那么性能将会受到很大影响,这时由比 TaskCreationOptions.LongRunning 更好的办法:

  • 如果任务是 I/O-Bound,TaskCompletionSource 和异步函数可以用回调(Coninuations)代替线程来实现并发。(杨老师说后续填坑)
  • 如果任务是 Compute-Bound,生产者/消费者队列允许对任务的并发性进行限流,避免把其它线程和进程饿死。(并发编程,后续了解)
Task 的返回值

Task 有一个泛型子类叫做 Task ,它允许发出一个返回值。

使用 Func 委托或者兼容的 Lambda 表达式来调用 Task.Run 就可以得到 Task

随后,可以通过 Result 属性来获得返回的结果。如果,这个 task 还没有完成操作,访问 Result 属性会阻塞该线程直到该 task 完成操作。

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

Task 可以看作是一种所谓的“未来/许诺”(future、promise),在它里面包裹这一个 Result,在稍后的时候就会变得可用。在 CTP 版本的时候,Task 就叫做 Future

Task 的异常

Task 与 Thread 不一样,Task 可以很方便的传播异常。

  • 如果 task 里面抛出了一个未处理的异常(故障),那么该异常就会重新抛出给:

    • 调用了 wait() 的地方

    • 访问了 Task 的 Result 属性的地方

      public 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;
              }
          }
      }
      

      输出结果:

      ​ Null

      CLR 将异常包裹在 AggregateException 里,一边在并行编程场景中发挥很好的作用。

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

    • 如果两个属性都返回 false,那么没有错误发生。
    • 如果 IsCanceled 为 true,那就说明一个 OperationCanceledException 为该 Task 抛出了。
    • 如果 IsFaulted 为 true,那就说明另一个类型的异常被抛出了,而 Exception 属性也将指明错误。
异常与“自治”的Task
  • 自治的,”设置完就不管了“的Task。就是指不通过调用 Wait() 方法、Result 属性或 continuation 进行合并的任务。
  • 针对自治的 Task,需要像 Thread 一样,显式的处理异常,避免发生”悄无声息的异常“
  • 自治 Task 上未处理的异常成为未观察到的异常。
未观察到的异常
  • 可以通过全局的 TaskScheduler.UnobservedTaskException 来订阅未观察到的异常。
  • 关于什么是”未观察到的异常“,由一些细微的差别:
    • 使用超时进行等待的 Task,如果在超时后发生故障,那么它将会产生一个”未观察到的异常“
    • 在 Task 发生故障后,如果访问 Task 的Exception 属性,那么该异常就被认为是“已观察到的”

Continuation

一个 Continuation 会对 Task 说:“当你结束的时候,继续做点其它的事。”

Continuation 通常是通过回调的方式实现的:

  • 当操作一结束,就开始执行

    public static void Main(string[] args)
    {
        Task<int> primeNumberTask = Task.Run(() =>
    		Enumerable.Range(2, 3000000).Count(n => 
            	Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
        
        var awaiter = primeNumberTask.GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            int result = awaiter.GetResult();
            Console.WriteLine(result);
        });
        Console.ReadLine();  //阻塞应用进程
    }
    
  • 在 task 上调用 GetAwaiter 会返回一个 awaiter 对象。它的 OnCompleted 方法会告诉之前的 task:“当你结束/发生故障的时候要执行委托”。

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

awaiter

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

  • OnCompleted() 方法
  • GetResult() 方法
  • IsCompleted 属性,该属性为 bool 类型

awaiter没有接口或者父类来统一这些成员。其中 OnCompleted 是 INotifyCompletion 接口的一部分。

如果发生故障

如果之前的任务发生故障

  • 当 Continuation 代码调用 awaiter.GetResult() 的时候,异常会被重新抛出。
  • 当无需调用 GerResult 时,我们可以直接访问 task 的 Result 属性。
  • 但调用 GetResult 的好处是,如果 task 发生故障,异常会被直接地抛出,而不是包裹在 AggregateException 里面,这样的话 catch 语句块就简介很多了。
非泛型task

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

同步上下文

如果同步上下文出现了,那么 OnCompleted 会自动捕获它,并将 Continuation 提交到这个上下文中。这一点在富客户端应用中非常有用,因为它会把 Continuation 放回到 UI 线程中。

如果是编写的一个库,则不希望出现上述行为,因为开销较大的 UI 线程切换应该在程序运行离开库的时候只发生一次,而不是出现在方法调用之间。所以,我们可以使用 ConfigureAwait 方法来避免这种行为。

public static void Main(string[] args)
{
    Task<int> primeNumberTask = Task.Run(() =>
		Enumerable.Range(2, 3000000).Count(n => 
        	Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    
    var awaiter = primeNumberTask.ConfigureAwait(false).GetAwaiter();
    awaiter.OnCompleted(() =>
    {
        int result = awaiter.GetResult();
        Console.WriteLine(result);
    });
    Console.ReadLine();  //阻塞应用进程
}

如果没有同步上下文出现,或者使用的是 ConfigureAwait(false),那么 Continuation 会运行在先前 task 的同一个线程上,从而避免不必要的开销。

ContinueWith

另外一种附件 Continuation 的方式就是调用 task 的 ContinueWith 方法

public static void Main(string[] args)
{
    Task<int> primeNumberTask = Task.Run(() =>
		Enumerable.Range(2, 3000000).Count(n => 
        	Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    
    primeNumberTask.ContinueWith(task =>
    {
        int result = awaiter.GetResult();
        Console.WriteLine(result);
    });
    Console.ReadLine();  //阻塞应用进程
}

ContinueWith 本身返回一个 task,它可以用它来附加更多的 Continuation。但是,必须直接处理 AggregateException:

  • 如果 task 发生故障,需要写额外的代码来把 Continuation 封装(marshal)到 UI 应用上。
  • 在非 UI 上下文中,若想让 Continuation 和 task 执行在同一个线程上,必须指定 TaskContinuationOptions.ExecuteSynchronously,否则它将弹回到线程池。

ContinueWith 对于并行编程来说是非常有用的。

TaskCompletionSource

创建 Task 除了通过 Task.Run(),还可以使用 TaskCompletionSource 来创建。

TaskCompletionSource 可以在稍后开始和结束的任意操作中创建 Task

  • 它会提供一个可手动执行的“从属” Task,可指示操作何时结束或发生故障

TaskCompletionSource 对于 IO-Bound 类工作比较理想

  • 可以获得所有 Task 的好处(传播值、异常、Continuation等)
  • 不需要在操作时阻塞线程
使用

使用TaskCompletionSource初始化一个实例即可。它有一个Task属性可返回一个Task 。该Task完全由TaskCompletionSource 对象控制 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G6rTubgm-1604878145491)(C:\Users\86183\Desktop\学习笔记\图片\image-20201105234308773.png)]

TaskCompletionSource 中有如上图的方法,调用人一个方法都会给 Task 发信号(故障、完成、取消)。这些方法只能调用一次,如果再次调用,则:

  • SetXxx 会抛出异常
  • TryXxx 会返回 false
public static void Main(string[] args)
{
    var tcs = new TaskCompletionSource<int>();
    
    new Thread(() =>
    {
        Thread.Sleep(5000);
        tcs.SetResult(42);
    })
    {
        IsBackground = true;
    }.Start();
    
    Task<int> task = tcs.Task;
    Console.WriteLine(task.Result);
}

自定义 Task.Run() 方法

public static void Main(string[] args)
{
    Task<int> task = Run(() =>
                         {
                             Thread.Sleep(5000);
                             return 42;
                         });
}

//调用这个方法相当于调用 Task.Factory.StartNew
//并使用 TaskCreationOptions.LongRunning 选项来创建非线程池的线程
static Task<TResult> Run<TResult>(Func<TResult> function)
{
    var tcs = new TaskCompletionSource<TResult>();
    new Thread(() =>
               {
                   try
                   {
                       tcs.SetResult(function());  //运行 function 委托
                   }
                   catch(System.Exception ex)
                   {
                       tcs.SetException(ex);
                   }
               }).Start();
    return tcs.Task;
}
TaskCompletionSource的真正魔力

TaskCompletionSource 创建 Task,但并不占用线程。

public static void Main(string[] args)
{
    var awaiter = GetAnswerToLife().GetAwaiter(); //生成一个 Continuation
    awaiter.OnCompleted(() => 
    {
        Console.WriteLine(awaiter.GetResult());
    });
    Console.ReadKey();
}

//调用这个方法相当于调用 Task.Factory.StartNew
//并使用 TaskCreationOptions.LongRunning 选项来创建非线程池的线程
static Task<int> GetAnswerToLife()
{
    var tcs = new TaskCompletionSource<int>();
    
    var timer = new System.Timers.Timer(5000) { AutoReset = false };
    timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };
    timer.Start();
    return tcs.Task;
}

将上述代码封装成更加通用的方法:

public static void Main(string[] args)
{
    Delay(5000).GetAwaiter().OnCompleted(() => Console.WriteLine(42));
    // 5秒钟之后,Continuation 开始的时候,才占用线程
}

//注意:因为没有非泛型版本的 TaskCompletionSource,此处用object妥协
static Task Delay(int milliseconds)
{
    var tcs = new TaskCompletionSource<object>();    
    var timer = new System.Timers.Timer(milliseconds) { AutoReset = false };
    timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };
    timer.Start();
    return tcs.Task;
}
Task.Delay

.net Core 自带方法,相当于异步版本的 Thread.Sleep 方法

public static void Main(string[] args)
{
    Delay(5000).GetAwaiter().OnCompleted(() => Console.WriteLine(42));
    // 5秒钟之后,Continuation 开始的时候,才占用线程
    
    Task.Delay(5000).GerAwaiter().OnCompleted(() => Console.WriteLine(42));
    Task.Delay(5000).ContinueWith(ant => Console.WriteLine(42));
    // Task.Delay 相当于异步版本的 Thread.Sleep
}

//注意:因为没有非泛型版本的 TaskCompletionSource,此处用object妥协
static Task Delay(int milliseconds)
{
    var tcs = new TaskCompletionSource<object>();    
    var timer = new System.Timers.Timer(milliseconds) { AutoReset = false };
    timer.Elapsed += delegate { timer.Dispose(); tcs.SetResult(42); };
    timer.Start();
    return tcs.Task;
}

你可能感兴趣的:(C#,#,C#异步编程,c#,.net)