处理异步任务时,使用await与使用ContinueWith有何不同?

Sup*_*Bob 8 .net c# multithreading task .net-core

这就是我的意思:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return Task.FromResult(new SomeObject()
            {
                IsAuthorized = false
            });
        }
        else
        {
            return repository.GetSomeObjectByTokenAsync(token).ContinueWith(t =>
            {
                t.Result.IsAuthorized = true;
                return t.Result;
            });
        }
    }
Run Code Online (Sandbox Code Playgroud)

可以等待上述方法,我认为它与基于TA同步P模式建议的操作非常相似?(我知道的其他模式是APMEAP模式。)

现在,下面的代码呢?

public async Task<SomeObject> GetSomeObjectByToken(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return new SomeObject()
            {
                IsAuthorized = false
            };
        }
        else
        {
            SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
            result.IsAuthorized = true;
            return result;
        }
    }
Run Code Online (Sandbox Code Playgroud)

这里的主要区别在于方法是async并且它使用await关键字-与以前编写的方法相比,这有什么变化?我知道也可以-等待。除非我弄错,否则返回Task的任何方法都可以。

每当方法标记为时async,我就知道await使用这些switch语句创建的状态机,并且我知道其本身不使用线程-它根本不会阻塞,线程只是去做其他事情,直到它被回叫以继续执行以上代码。

但是,当我们使用await关键字调用它们时,这两种方法之间的根本区别是什么?根本没有任何区别,如果有-首选哪个?

编辑:我觉得第一个代码段是首选,因为我们有效地忽略了async / await关键字,而没有任何影响-我们返回一个将继续同步执行的任务,或者在热路径上已经完成的任务(可以是已缓存)。

ace*_*ent 5

async/ await机制使编译器将您的代码转换成一个状态机。您的代码将同步运行,直到第一个await命中尚未完成的等待代码(如果有)。

在Microsoft C#编译器中,此状态机是一种值类型,这意味着当所有状态机都已await完成等待时,它将具有非常小的成本,因为它不会分配对象,因此不会生成垃圾。当任何等待未完成时,此值类型将不可避免地被装箱。

请注意,Task如果这是await表达式中使用的等待类型,则这不会避免分配。

使用ContinueWith,只有Task在您的延续没有闭包并且您不使用状态对象或尽可能多地重用状态对象(例如从池中)时,才避免分配(除外)。

同样,在任务完成时调用延续,创建堆栈框架,但不会内联。该框架试图避免堆栈溢出,但是在某些情况下,它可能会避免堆栈溢出,例如在分配大数组时。

它试图避免这种情况的方法是检查剩余的堆栈数量,如果通过某种内部措施将堆栈视为已满,则它会安排继续在任务调度程序中运行。它试图以性能为代价避免致命的堆栈溢出异常。

async/ await和之间有一个细微的区别ContinueWith

  • async/ await将在(SynchronizationContext.Current如果有的话)中安排继续,否则在TaskScheduler.Current 1

  • ContinueWith将在提供的任务计划程序中或在TaskScheduler.Current没有任务计划程序参数的重载中计划继续

模拟async/ await的默认行为:

.ContinueWith(continuationAction,
    SynchronizationContext.Current != null ?
        TaskScheduler.FromCurrentSynchronizationContext() :
        TaskScheduler.Current)
Run Code Online (Sandbox Code Playgroud)

为了模拟async/ await“s的行为TaskS” .ConfigureAwait(false)

.ContinueWith(continuationAction,
    TaskScheduler.Default)
Run Code Online (Sandbox Code Playgroud)

循环和异常处理开始使事情变得复杂。除了使您的代码可读性外,async/还await可以与任何可等待的一起使用

您的情况最好用混合方法处理:同步方法,在需要时调用异步方法。使用此方法的代码示例:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
{
    string token = repository.GetTokenById(id);
    if (string.IsNullOrEmpty(token))
    {
        return Task.FromResult(new SomeObject()
        {
            IsAuthorized = false
        });
    }
    else
    {
        return InternalGetSomeObjectByTokenAsync(repository, token);
    }
}

internal async Task<SomeObject> InternalGetSomeObjectByToken(Repository repository, string token)
{
    SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
    result.IsAuthorized = true;
    return result;
}
Run Code Online (Sandbox Code Playgroud)

以我的经验,我发现在应用程序代码中很少有地方可以增加这种复杂性,而这些地方实际上可以腾出时间来开发,审查和测试这种方法,而在代码中,任何方法都可能成为瓶颈。

我倾向于执行任务的唯一情况是a Task或return Task<T>方法仅返回另一个异步方法的结果,而其自身未执行任何I / O或任何后处理。

YMMV。


  1. 除非您使用ConfigureAwait(false)或等待使用自定义计划的某些等待

  • @TheodorZoulias,[生成的源代码](https://sharplab.io/#v2:D4AQTAjAsAUCAMACEECsBuWsQGZlmQgHZEBvWRSxCq3QgNmQA5lGBZAQwEsA7ACgCUNSuRhVxGAJysAdAE0uEA=A=YY=um释放。) (3认同)
  • @SpiritBob,如果您真的想摆弄任务结果,我想没关系,`.ContinueWith`应该用于编程方式。尤其是在处理CPU密集型任务而不是I / O任务时。但是当您不得不处理Task的属性,AggregateException以及处理更多异步方法时处理条件,循环和异常处理的复杂性时,几乎没有任何理由坚持使用它。 (2认同)
  • @SpiritBob,对不起,我的意思是“循环”。是的,您可以使用Task.FromResult(“ ...”)`创建一个静态只读字段。在异步代码中,如果按键缓存需要I / O获得的值,则可以使用其中值为任务的字典,例如`ConcurrentDictionary &lt;string,Task &lt;T &gt;&gt;`而不是`ConcurrentDictionary &lt;string,T&gt;使用带有工厂函数的GetOrAdd调用,然后等待其结果。这样可以确保仅发出一个I / O请求来填充高速缓存的密钥,一旦任务完成,它就会唤醒等待者,然后用作完成的任务。 (2认同)