前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >c#异步编程-Task(一)

c#异步编程-Task(一)

作者头像
JusterZhu
发布2022-12-07 18:22:17
5810
发布2022-12-07 18:22:17
举报
文章被收录于专栏:JusterZhuJusterZhu

一、概要

大家好,本次继续分享自己的学习经历。本文主要分享异步编程中Task的使用,如果能帮助大家希望多多关注文章末尾的微信公众号和知乎三连。各位举手之劳是对我更新技术文章最大的支持。

  • 个人心得:Task是一个升级版本的Thread的类,它非常的灵活支持取消、阻塞等待、合并多个Task协同操作、编码高效易懂、异常传播、回调传递结果或调用方法等。
  • 本文相关文献查阅地址:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task-1?f1url=%3FappId%3DDev16IDEF1%26l%3DZH-CN%26k%3Dk(System.Threading.Tasks.Task);k(DevLang-csharp)%26rd%3Dtrue&view=net-5.0

二、详细内容

1.Task

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

  • 虽然开始线程的时候可以方便的传入数据,但是当join的时候很难从线程获得返回值。
  • 可能需要设置一些共享字段。
  • 如果操作抛出异常,铺货和传播该异常都很麻烦
  • 无法告诉线程在结束时开始另外的工作,你必须进行join操作(在进程中阻塞当前的线程)
  • 很难使用较小的并发(concurrent)来组件大型的并发

Task类可以很好的解决上述问题,它是一个高级抽象:它代表了一个并发操作(concurrent),该操作可能有Thread支持,或不由Thread支持。

  • Task是可组合的(可使用continuation把他们穿成链)。
  • Tasks可以使用线程池来减少启动延迟。
  • 使用TaskCompletionSource,Tasks可以利用回调的方式,在等待I/O绑定操作时完全避免使用线程。

开始一个Task ,Task.Run

开始一个Task最简单的办法就是使用Task.Run(.net4.5,4.0的时候是Task.Factory.StartNew)传入一个Action委托即可(例子task)

代码语言:javascript
复制
Task.Run(()=>{  Console.WriteLine("do it"); });
  • Task默认使用线程池,也就是后台线程:当主线程结束时,你创建所有的tasks都会结束。
  • Task.Run返回一个Task对象,可以使用它来监视其过程
  • 在Task.Run之后,我们没有调用Start,因为该方法创建的是“热”任务(hot task)
  • 可以通过task的构造函数创建“冷”任务(cold task),但开发中很少这么干
  • 通过Task的Status属性来跟踪task的执行状态。

Task.Status枚举状态如下这里就不详细分析可以去官方文档查阅具体用法:

代码语言:javascript
复制
public enum TaskStatus
{
    //
    // 摘要:
    //     The task has been initialized but has not yet been scheduled.
    Created = 0,
    //
    // 摘要:
    //     The task is waiting to be activated and scheduled internally by the .NET Framework
    //     infrastructure.
    WaitingForActivation = 1,
    //
    // 摘要:
    //     The task has been scheduled for execution but has not yet begun executing.
    WaitingToRun = 2,
    //
    // 摘要:
    //     The task is running but has not yet completed.
    Running = 3,
    //
    // 摘要:
    //     The task has finished executing and is implicitly waiting for attached child
    //     tasks to complete.
    WaitingForChildrenToComplete = 4,
    //
    // 摘要:
    //     The task completed execution successfully.
    RanToCompletion = 5,
    //
    // 摘要:
    //     The task acknowledged cancellation by throwing an OperationCanceledException
    //     with its own CancellationToken while the token was in signaled state, or the
    //     task's CancellationToken was already signaled before the task started executing.
    //     For more information, see Task Cancellation.
    Canceled = 6,
    //
    // 摘要:
    //     The task completed due to an unhandled exception.
    Faulted = 7
}

if (task.Status == TaskStatus.RanToCompletion)
{
    //当当前线程状态表示完成时则执行后续操作
    Console.WriteLine("do it");
}

Task.Wait等待

调用task的wait方法会进行阻塞直到操作完成,相当于thread上的join方法。

代码语言:javascript
复制
Task mytask = Task.Run(()=> 
{
    Thread.Sleep(3000);
    Console.WriteLine("do it");
});
Console.WriteLine(mytask.IsCanceled);//false
mytask.Wait();//阻塞主线程直到mytask执行完毕
Console.WriteLine(mytask.IsCanceled);//true

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

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

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

针对长时间允许的任务或阻塞操作,你可以不用采用线程池

代码语言:javascript
复制
Task task = Task.Factory.StartNew(()=> 
{
    Thread.Sleep(3000);
    Console.WriteLine("do it");
},TaskCreationOptions.LongRunning);

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

  • 如果任务是IO-Bound,TaskCompletionSource和异步函数可以让你用回调(Coninuations)代替线程来实现并发。
  • 如果任务是Compute-Bound,生产者/消费者队列允许你对任务的并发性限流,避免把其他的线程和进程的CPU处理时间片占尽。

2.Task的返回值

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

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

随后,可以通过Result属性来获得返回的结果。

  • 如果这个task还没有完成操作,访问Result属性会阻塞该线程知道该task完成操作。 Task<int> task = Task.Run(()=> { Console.WriteLine("do it"); return 666; }); int result = task.Result; Console.WriteLine(result);

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

在CTP版本的时候,Task实际上叫做Future


3.Task的异常

与Thread不一样,Task可以很方便的传播异常 如果你的task里面抛出了一个未处理的异常,那么该异常就会重新被抛出给:

  • 调用了wait()的地方
  • 访问了Task 的Reuslt属性的地方。

代码如下:

代码语言:javascript
复制
Task mytask = Task.Run(()=> { throw null; });
try
{
    mytask.Wait();
}
catch (AggregateException aex)
{
    if (aex.InnerExceptions is NullReferenceException)
    {
        Console.WriteLine("null");
    }
    else
    {
        throw;
    }
}

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

如果我们不想抛出异常就想知道task有没有发生故障,无需重新抛出异常,通过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属性,那么该异常就被认为是“已观察到的”。

4.Coninuation

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

代码如下:

代码语言:javascript
复制
    Task<int> mytask = Task.Run(() => 
    { 
        Console.WriteLine("do it");
        return 666;
    });
    var awaiter =  mytask.GetAwaiter();
    awaiter.OnCompleted(()=> 
    {
        int result = awaiter.GetResult();
        Console.WriteLine(result);
    });
  • 在Task上调用GetAwaiter会返回一个awaiter对象
    • 它的OnCompleted方法会告诉之前的task:“当结束/发生故障的时候要执行委托” 。
  • 可以将Continuation附加到已经结束的task上面,此时continuation将会被安排立即执行。

awaiter

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

如果发生故障

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

非泛型task

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

同步上下文

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

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

代码语言:javascript
复制
Task<int> mytask = Task.Run(() => 
{ 
    Console.WriteLine("do it");
    return 666;
});
var awaiter =  mytask.ConfigureAwait(false).GetAwaiter();
awaiter.OnCompleted(()=> 
{
    int result = awaiter.GetResult();
    Console.WriteLine(result);
});

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

ContinueWith

另外一种附加Continuation的方式就是调用task的Continuewith方法。

代码语言:javascript
复制
Task<int> mytask = Task.Run(() => 
{ 
    Console.WriteLine("do it");
    return 666;
});

mytask.ContinueWith(task=> 
{
    int result = task.Result;
    Console.WriteLine(result);
});

Continuewith本身返回一个task,它可以用它来附加更多的Continuation。

但是,必须直接处理AggregateException:

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

5.TaskCompletionSource

  • TaskCompletionSource也可以用来创建Task
  • TaskCompletionSource让你在稍后开始和结束的任意操作中创建Task
    • 它会为你提供一个可手动执行的“从属”Task
    • 只是操作合适结束或发生故障
  • 它对IO-Bound类工作比较理想
    • 可以获得所有Task的好处(传播至、异常、Continuation等)
    • 不需要在操作时阻塞线程
  • 初始化一个实例即可
  • 它有一个Task属性可返回一个Task
  • 该Task完全由TaskCompletionSource对象控制
  • 调用任意一个方法都会给Task发信号:
    • 完成、故障、取消
  • 这些方法只能调用一次,如果再次调用:
    • SetXXX会抛出异常
    • TryXXX会返回false

方法源码如下:

代码语言:javascript
复制
public class TaskCompletionSource<TResult>
{
    public TaskCompletionSource();

    public TaskCompletionSource(object? state);

    public TaskCompletionSource(TaskCreationOptions creationOptions);

    public TaskCompletionSource(object? state, TaskCreationOptions creationOptions);

    public Task<TResult> Task { get; }

    public void SetCanceled();

    public void SetException(IEnumerable<Exception> exceptions);

    public void SetException(Exception exception);

    public void SetResult(TResult result);

    public bool TrySetCanceled();

    public bool TrySetCanceled(CancellationToken cancellationToken);

    public bool TrySetException(IEnumerable<Exception> exceptions);

    public bool TrySetException(Exception exception);

    public bool TrySetResult(TResult result);
}

使用示例代码:

代码语言:javascript
复制
/*
 *CODE1
 */
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);


/*CODE2
 * 调用此方法相当于调用Task.Factory.StartNew
 * 并使用TaskCreationOptions.LongRunning选项来创建非线程池的线程
 */
Task<TResult> Run<TResult>(Func<TResult> func) 
{
    var tcs = new TaskCompletionSource<TResult>();
    new Thread(() =>
    {
        try
        {
            tcs.SetResult(func());
        }
        catch (Exception ex)
        {

            tcs.SetException(ex);
        }
    })
    {
        IsBackground = true
    }.Start();
    return tcs.Task;
}

TaskCompletionSource终极奥义

  • TaskCompletionSource自身创建Task,但并不占用线程(见示例代码)
  • 特别需要说明的一点,Task中的Delay和Thread的Sleep不一样的是,Sleep不占用CPU处理资源而Delay会,因为它只是延迟了几秒执行代码而已。

示例代码:

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

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;
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JusterZhu 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、概要
  • 二、详细内容
    • 1.Task
      • 开始一个Task ,Task.Run
      • Task.Wait等待
      • Long-running tasks 长时间运行的任务
    • 2.Task的返回值
      • 3.Task的异常
        • 异常与“自治”的Task
        • 未观察到的异常
      • 4.Coninuation
        • awaiter
        • 如果发生故障
        • 非泛型task
        • 同步上下文
        • ContinueWith
      • 5.TaskCompletionSource
        • TaskCompletionSource终极奥义
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档