简体   繁体   English

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

[英]How does using await differ from using ContinueWith when processing async tasks?

Here's what I mean:这就是我的意思:

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

Above method can be awaited and I think it closely resembles to what the T ask-based A synchronous P attern suggests doing?可以等待上述方法,我认为它与基于T询问的A同步P模式建议的做法非常相似? (The other patterns I know of are the APM and EAP patterns.) (我知道的其他模式是APMEAP模式。)

Now, what about the following code:现在,下面的代码呢:

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

The key differences here are that the method is async and it utilizes the await keywords - so what does this change in contrast to the previously written method?这里的主要区别在于该方法是async的并且它使用了await关键字——那么与之前编写的方法相比,这有什么变化呢? I know it can too - be awaited.我知道它也可以——等待。 Any method returning Task can for that matter, unless I'm mistaken.除非我弄错了,否则任何返回 Task 的方法都可以。

I'm aware of the state machine created with those switch statements whenever a method is labeled as async , and I'm aware that await itself uses no thread - it doesn't block at all, the thread simply goes to do other things, until it's called back to continue execution of the above code.我知道 state 机器只要方法被标记为async就使用这些 switch 语句创建,并且我知道await本身不使用线程 - 它根本不会阻塞,线程只是去做其他事情,直到它被回调以继续执行上述代码。

But what's the underlying difference between the two methods, when we invoke them using the await keyword?但是,当我们使用await关键字调用它们时,这两种方法之间的根本区别是什么? Is there any difference at all, and if there is - which is preferred?有什么区别吗?如果有 - 哪个是首选?

EDIT: I feel like the first code snippet is preferred, because we effectivelyelide the async/await keywords, without any repercussions - we return a task that will continue its execution synchronously, or an already completed task on the hot path (which can be cached).编辑:我觉得第一个代码片段是首选,因为我们有效地省略了 async/await 关键字,没有任何影响 - 我们返回一个将继续同步执行的任务,或者热路径上已经完成的任务(可以是缓存)。

The async / await mechanism makes the compiler transform your code into a state machine. async / await机制使编译器将您的代码转换为 state 机器。 Your code will run synchronously until the first await that hits an awaitable that has not completed, if any.您的代码将同步运行,直到第一个await命中尚未完成的可等待对象(如果有)。

In the Microsoft C# compiler, this state machine is a value type, which means it will have a very small cost when all await s get completed awaitables, as it won't allocate an object, and therefore, it won't generate garbage.在微软的 C# 编译器中,这个 state 机器是一个值类型,这意味着当所有的await s 都完成 awaitables 时,它的成本非常低,因为它不会分配 ZA8CFDE6331BD59EB2AC96F8911,因此它不会生成垃圾。6,因此它不会生成垃圾。 When any awaitable is not completed, this value type is inevitably boxed.当任何 awaitable 未完成时,此值类型不可避免地会被装箱。 1 1

Note that this doesn't avoid allocation of Task s if that's the type of awaitables used in the await expressions.请注意,如果这是await表达式中使用的可等待对象的类型,这并不能避免分配Task

With ContinueWith , you only avoid allocations (other than Task ) if your continuation doesn't have a closure and if you either don't use a state object or you reuse a state object as much as possible (eg from a pool). With ContinueWith , you only avoid allocations (other than Task ) if your continuation doesn't have a closure and if you either don't use a state object or you reuse a state object as much as possible (eg from a pool).

Also, the continuation is called when the task is completed, creating a stack frame, it doesn't get inlined.此外,任务完成时调用延续,创建一个堆栈帧,它不会被内联。 The framework tries to avoid stack overflows, but there may be a case where it won't avoid one, such as when big arrays are stack allocated.该框架试图避免堆栈溢出,但在某些情况下它可能无法避免,例如当大 arrays 被堆栈分配时。

The way it tries to avoid this is by checking how much stack is left and, if by some internal measure the stack is considered full, it schedules the continuation to run in the task scheduler.它试图避免这种情况的方法是检查剩下多少堆栈,如果通过某种内部措施认为堆栈已满,它会安排继续在任务调度程序中运行。 It tries to avoid fatal stack overflow exceptions at the cost of performance.它试图以性能为代价避免致命的堆栈溢出异常。

Here is a subtle difference between async / await and ContinueWith :这是async / awaitContinueWith之间的细微差别:

  • async / await will schedule continuations in SynchronizationContext.Current if any, otherwise in TaskScheduler.Current 2 async / await将在SynchronizationContext.Current中安排延续(如果有),否则在TaskScheduler.Current 2

  • ContinueWith will schedule continuations in the provided task scheduler, or in TaskScheduler.Current in the overloads without the task scheduler parameter ContinueWith将在提供的任务调度程序中安排延续,或者在没有任务调度程序参数的重载中的TaskScheduler.Current

To simulate async / await 's default behavior:模拟async / await的默认行为:

.ContinueWith(continuationAction,
    SynchronizationContext.Current != null ?
        TaskScheduler.FromCurrentSynchronizationContext() :
        TaskScheduler.Current)

To simulate async / await 's behavior with Task 's .ConfigureAwait(false) :使用Task.ConfigureAwait(false)模拟async / await的行为:

.ContinueWith(continuationAction,
    TaskScheduler.Default)

Things start to get complicated with loops and exception handling.循环和异常处理使事情开始变得复杂。 Besides keeping your code readable, async / await works with any awaitable .除了保持代码可读性之外, async / await还可以与任何awaitable 一起使用

Your case is best handled with a mixed approach: a synchronous method that calls an asynchronous method when needed.您的情况最好使用混合方法处理:在需要时调用异步方法的同步方法。 An example of your code with this approach:使用这种方法的代码示例:

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

In my experience, I've found very few places in application code where adding such complexity actually pays off the time to develop, review and test such approaches, whereas in library code any method can be a bottleneck.以我的经验,我发现在应用程序代码中添加如此复杂性的地方很少能真正为开发、审查和测试此类方法的时间带来回报,而在代码中,任何方法都可能成为瓶颈。

The only case where I tend elide tasks is when a Task or Task<T> returning method simply returns the result of another asynchronous method, without itself having performed any I/O or any post-processing.我倾向于省略任务的唯一情况是TaskTask<T>返回方法仅返回另一个异步方法的结果,而本身没有执行任何 I/O 或任何后处理。

YMMV. YMMV。


  1. When building for Release, the compiler generates structs.在为 Release 构建时,编译器会生成结构。

    When building for Debug, the compiler generates classes to allow edit-and-continue on async code.在为 Debug 构建时,编译器会生成类以允许对异步代码进行编辑并继续。

  2. Unless you use ConfigureAwait(false) or await on some awaitable that uses custom scheduling.除非您使用ConfigureAwait(false)或在使用自定义调度的某些可等待对象上等待。

By using ContinueWith you are using the tools that where available before the introduction of the async / await functionality with C# 5 back at 2012. As a tool it is verbose, not easily composable, and requires extra work for unwrapping AggregateException s and Task<Task<TResult>> return values (you get these when you pass asynchronous delegates as arguments).通过使用ContinueWith ,您正在使用在 2012 年推出 C# 5 的async / await功能之前可用的工具。作为一种工具,它很冗长,不易组合,并且需要额外的工作来解开AggregateExceptionTask<Task<TResult>>返回值(当您将异步委托作为参数传递时会得到这些值)。 It offers few advantages in return.作为回报,它提供的优势很少。 You may consider using it when you want to attach multiple continuations to the same Task , or in some rare cases where you can't use async / await for some reason (like when you are in a method with out parameters ).当您想将多个延续附加到同一个Task时,或者在某些极少数情况下由于某种原因(例如在out参数的方法中)无法使用async / await时,您可以考虑使用它。


Update: I removed the misleading advice that the ContinueWith should use the TaskScheduler.Default to mimic the default behavior of await .更新:我删除了ContinueWith应该使用TaskScheduler.Default来模仿await的默认行为的误导性建议。 Actually the await by default schedules its continuation using TaskScheduler.Current . 实际上,默认情况下await使用TaskScheduler.Current安排其继续。

Difference between await and ContinueWith await 和 ContinueWith 之间的区别

Methods that perform asynchronous operations don't need to use await if:在以下情况下,执行异步操作的方法不需要使用 await:

There is only one asynchronous call inside the method.方法内部只有一个异步调用。 The asynchronous call is at the end of the method.异步调用在方法的末尾。 Catching/handling exception that may happen within the Task is not necessary.不需要捕获/处理任务中可能发生的异常。

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

If GetSomeObjectByTokenAsync has the same signature as GetSomeObjectByTokenAsync (returning a Task), the method can be simplified:如果 GetSomeObjectByTokenAsync 与 GetSomeObjectByTokenAsync 具有相同的签名(返回一个 Task),则方法可以简化:

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

In this case, the method doesn't need to be marked async, even though it's preforming an asynchronous operation.在这种情况下,该方法不需要标记为异步,即使它正在执行异步操作。 The Task returned by GetSomeObjectByTokenAsync is passed directly to the calling method, where it will be awaited. GetSomeObjectByTokenAsync 返回的 Task 直接传递给调用方法,在那里等待它。

Returning the Task instead of awaiting it, changes the exception behavior of the method, as it won't throw the exception inside the method which starts the task but in the method which awaits it.返回任务而不是等待它,会改变方法的异常行为,因为它不会在启动任务的方法中抛出异常,而是在等待它的方法中抛出异常。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM