最后一个await时不必要使用异步/等待吗?

21

最近我一直在处理与 async await 相关的问题(阅读了所有可能的文章,包括Stephen和Jon的最后两章),但我得出了结论,不知道是否正确-因此我的问题。

既然async只允许存在单词await,那么我将离开async

AFAIU,await关乎持续性。不要编写函数式(连续性)代码,而是编写同步代码(我喜欢将其称为可回调的代码)。

所以当编译器到达await时,它将代码分为两个部分并注册第二部分在第一部分完成后执行(我不知道为什么不使用单词callback-这正是所做的)。 (同时在工作-线程正在做其他事情)。

但是看看这段代码:

public async  Task ProcessAsync()
        {
           Task<string> workTask = SimulateWork();
           string st= await workTask;
           //do something with st
        }

 public    Task <string> SimulateWork()
        {
            return ...
        }

当线程到达await workTask;时,它将该方法分为两个部分。因此,在SimulateWork完成后,方法的继续部分:即//do something with st将被执行。

一切正常。

但是如果这个方法是:

public async  Task ProcessAsync()
        {
           Task<string> workTask = SimulateWork();
           await workTask; //i don't care about the result , and I don't have any further commands 
        }

这里 - 我不需要继续,这意味着我不需要使用 await 来分割方法,也就是说,我根本不需要在这里使用 async/await!即使如此,我仍然会得到相同的结果和行为!

因此,我可以这样做:

   public void ProcessAsync()
            {
               SimulateWork();
            }

问题:

  • 我的诊断结果是否100%正确?

4
为了让翻译完全纯净,您仍应该返回Task而不是void,并且应该返回由SimulateWork返回的Task - 这样,ProcessAsync调用方仍然可以使用await(或Wait)在那个Task上,以知道何时完成。 - Damien_The_Unbeliever
3
你应该始终关注任务的完成情况。 - Paulo Morgado
1
@PauloMorgado 我不同意这个说法。在我的示例中,只有在我需要知道ProcessAsync发生了什么时,它才是相关的。确实存在一些情况下,fire and forget是合法的。 - Royi Namir
1
如果任务出现错误会发生什么? - Paulo Morgado
1
就像后台线程执行时,您会在该线程内处理错误一样(使用try catch)。如果关于连续性和A依赖于B非常重要 - 那么像我说的那样 - 使用await/continue。 - Royi Namir
显示剩余2条评论
2个回答

19

所以,您认为下面的await是多余的,就像问题的标题所暗示的那样:

public async Task ProcessAsync()
{
    Task<string> workTask = SimulateWork();
    await workTask; //i don't care about the result , and I don't have any further 
}

首先,我假设在“当 await 在最后”下面,您指的是“当 await 是唯一的 await”。必须是这样,否则以下内容将无法编译:
public async Task ProcessAsync()
{
    await Task.Delay(1000);
    Task<string> workTask = SimulateWork();
    return workTask; 
}

现在,如果它是唯一的await,您确实可以像这样优化它:

public Task ProcessAsync()
{
    Task<string> workTask = SimulateWork();
    return workTask; 
}

然而,这将给你完全不同的异常传播行为,可能会产生一些意外的副作用。事实是,现在异常可能会在调用者的堆栈上抛出,这取决于SimulateWork的内部实现方式。我发布了一个详细说明此行为的文章。这通常不会发生在async Task/Task<>方法中,其中异常存储在返回的Task对象中。对于async void方法仍然可能发生,但那是另一回事

因此,如果您的调用代码已准备好处理异常传播中的这些差异,那么只要可能就跳过async/await,直接返回一个Task可能是个好主意。

另一件事是如果您想发出fire-and-forget调用。通常情况下,您仍然希望以某种方式跟踪已启动任务的状态,至少出于处理任务异常的原因。即使所有任务只是记录日志,我也无法想象真正不关心任务是否完成的情况。

因此,对于fire-and-forget,我通常会使用一个帮助器async void方法,它将挂起的任务存储在某个地方以供稍后观察,例如:

readonly object _syncLock = new Object();
readonly HashSet<Task> _pendingTasks = new HashSet<Task>();

async void QueueTaskAsync(Task task)
{
    // keep failed/cancelled tasks in the list
    // they will be observed outside
    lock (_syncLock)
        _pendingTasks.Add(task);

    try
    {
        await task;
    }
    catch
    {
        // is it not task's exception?
        if (!task.IsCanceled && !task.IsFaulted)
            throw; // re-throw

        // swallow, but do not remove the faulted/cancelled task from _pendingTasks 
        // the error will be observed later, when we process _pendingTasks,
        // e.g.: await Task.WhenAll(_pendingTasks.ToArray())
        return;
    }

    // remove the successfully completed task from the list
    lock (_syncLock)
        _pendingTasks.Remove(task);
}

你可以这样调用:

public Task ProcessAsync()
{
    QueueTaskAsync(SimulateWork());
}

目标是在当前线程的同步上下文中立即抛出致命异常(例如,内存不足),而任务结果/错误处理则会被延迟到适当的时间。

关于使用"fire-and-forget"任务的讨论可以在这里找到。


13

你很接近了。它的意思是你可以像这样写:

public Task ProcessAsync()
{
    // some sync code
    return SimulateWork();
}

这样做,您就不需要为将方法标记为async的开销付费,但仍然能够等待整个操作。


@RoyiNamir 如果你在提到 async void 事件处理程序,那么它们必须保持 async 以支持异步操作,不像 ProcessAsync 可以轻松地不标记为 async 而不会真正改变任何东西。 - i3arnon
是的,我知道,只是提到它们无法具有任务返回类型。 - Royi Namir
需要理解的重要部分是,正如arnon所提到的那样,你的无返回值方法将是不可等待的。调用代码的用户将无法根据void签名知道该方法是同步运行还是异步运行。 - Yuval Itzchakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接