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

此入门教程是记录下方参考资料视频的过程
开发工具: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组合器

Continuation 继续/延续

  1. 一个Continuation会对Task说:“当你结束的时候,继续再做点其它的事”
  2. Continuation通常是通过回调的方式实现的
    当操作一结束,就开始执行

例子

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);//Writes result
    });

    Console.ReadLine();
}
  1. 在task上调用GetAwaiter会返回一个awaiter对象
    它的OnCompleted方法会告诉之前的task:“当你结束/发生故障的时候要执行委托”。
  2. 可以将Continuation附加到已经结束的task上面,此时Continuation将会被安排立即执行

awaiter

  1. 任何可以暴露下列两个方法和一个属性的对象就是awaiter:
    OnCompleted
    GetResult
    一个叫做IsCompleted的bool属性
  2. 没有接口或者父类来统一这些成员
  3. 其中OnCompleted是INotifyCompletion的一部分

如果发生故障

  1. 如果之前的认为发生故障,那么当Continuation代码调用awaiter.GetResult()的时候,异常会被重新抛出
  2. 无需调用GetResult,我们可以直接访问task的Result属性
  3. 但调用GetResult的好处是,如果task发生故障,那么异常会被直接的抛出,而不是包裹在AggregateException里面,这样的话catch块就简洁很多了

非泛型task

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

同步上下文

  1. 如果同步上下文中出现了,那么OnCompleted会自动捕获它,并将Continuation提交到这个上下文中。这一点在富客户端应用中非常有用,因为它会把Continuation放回到UI线程中
  2. 如果是编写一个库,则不希望出现上述行为,因为开销较大的UI线程切换应该在程序运行离开库的时候只发生一次,而不是出现在方法调用之间。所以,我们可以使用ConfigureAwait方法来避免这种行为
    例子
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);//Writes result
    });

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

ContinueWith

  1. 另一种附加Continuation的方式就是调用task的ContinueWith方法
    例子
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 = task.Result;
        Console.WriteLine(result);//Writes result
    });

    Console.ReadLine();
}
  1. ContinueWith本身返回一个task,它可以用来附加更多的Continuation
  2. 但是必须直接处理AggregateException:
    如果task发生故障,需要写额外的代码来把Continuation封装(marshal)到UI应用上
    在非UI上下文中,若想让Continuation和task执行同一个线程上,必须指定TaskContinuationOptions.ExecuteSynchronously,否则它将弹回到线程池
  3. ContinueWith对于并行编程来说非常有用

TaskCompletionSource

  1. Task.Run创建Task
  2. 另一种方式就是用TaskCompletionSource来创建Task
  3. TaskCompletionSource让你在稍后开始和结束的任意操作中创建Task
    它会为你提供一个可手动执行的“从属”Task
       指示操作何时结束或发生故障
  4. 它对IO-Bound类工作比较理想
    可以获得所有Task的好处(传播值、异常、Continuation等)
    不需要在操作时阻塞线程

使用TaskCompletionSource

  1. 初始化一个实例即可
  2. 它有一个Task属性可以返回一个Task
  3. 该Task完全由TaskCompletionSource对象控制
  4. 调用任意一个方法都会给Task发信号:
    完成、故障、取消
  5. 这些方法只能调用一次,如果再次调用:
    SetXxx会抛出异常
    TryXxx会返回false

例子

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

例子

static void Main(string[] args)
{
    //此处的Run()是底下我们实现的Run()
    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
        {
            //function()的执行结果作为信号
            tcs.SetResult(function());
        }
        catch(System.Exception e)
        {
            tcs.SetException(e);
        }
    }).Start();
    return tcs.Task;
}

TaskCompletionSource的真正魔力

  1. 它创建Task,但并不占用线程

例子

static void Main(string[] args)
{
    var awaiter=GetAnswerToLife().GetAwaiter();
    awaiter.OnCompleted(()=>
    {
        Console.WriteLine(awaiter.GetResult());
    });
    //不会占用线程,所以不输出,需要阻塞
    Console.ReadKey();
}
static Task<int> GetAnswerToLife()
{
    var tcs=new TaskCompletionSource<int>();
    //AutoReset
    //如果 Timer 应在每次间隔结束时引发 Elapsed 事件,则为 true;如果它仅在间隔第一次结束后引发一次 Elapsed 事件,则为 false。 默认值为 true。 
    //如果调用 Start 方法时已经启用 Timer,则重置间隔。 如果 AutoReset 为 false,则必须调用 Start 方法才能再次开始计数。
    var timer=new System.Timers.Timer(5000){AutoReset=false};
    //时间到达指定间隔就会触发Elapsed事件
    timer.Elapsed+=delegate{timer.Dispose();tcs.SetResult(42);};
    timer.Start();
    return tcs.Task;
}

例子

static void Main(string[] args)
{
    Delay(5000).GetAwaiter().OnCompleted(()=>Console.WriteLine(42));
    //5秒钟之后,Continuation开始的时候,才占用线程,所以无输出
}
//注意:没有非泛型版本的TaskCompletionSource
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(null);};
    timer.Start();
    return tcs.Task;
}

实现 Task.Delay

例子

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

    //Delay()
    //在完成返回的任务前要等待的毫秒数;如果无限期等待,则为 -1。
    Task.Delay(5000).GetAwaiter().OnCompleted(()=>Console.WriteLine(42));
    Task.Delay(5000).ContinueWith(ant=>Console.WriteLine(42));
    //Task.Delay相当于异步版本的Thread.Sleep
}
//注意:没有非泛型版本的TaskCompletionSource
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(null);};
    timer.Start();
    return tcs.Task; 
}

Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay 结束

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