使用async而不使用await?

46

考虑在没有使用await的情况下使用async

我认为你可能误解了async的作用。警告是完全正确的:如果你标记了方法为async但在任何地方都没有使用await,那么你的方法将不会是异步的。如果你调用它,方法内部的所有代码将同步执行。

我想编写一个应该以异步方式运行但不需要使用await的方法。例如,在使用线程时。

public async Task PushCallAsync(CallNotificationInfo callNotificationInfo)
{
    Logger.LogInfo("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId,
}

我想调用PushCallAsync并异步运行,而不想使用await。
在C#中,我可以使用async而不使用await吗?

2
好的。这完全取决于您的设计问题在哪里。您希望“async”做什么? - J. Steen
6
如果你不打算等待这种方法,为什么要将其声明为异步?我认为你引用的文本已经很好地概括了这一点。 - BoltClock
1
我认为你可以直接使用 Task.Runasync/await 并不是真正的多线程机制,实际上我认为运行时会尽可能地在尽可能少的线程上执行任务。这主要是因为编译器会自动将你的代码转换为延续传递风格,所以只有在绝对必要时才需要等待后台操作的结果或完成。当你不需要等待(或 await)调用结果时,这不是你要寻找的语言特性。 - millimoose
4
“async” 关键字与线程的交互有一些复杂,而且默认行为很容易被覆盖。它并不是一个多线程机制,也不总是在单个线程上运行。我有一篇博客文章总结了“async”如何调度其继续执行:http://blog.stephencleary.com/2012/02/async-and-await.html。 - Stephen Cleary
2
@millimoose:这不是一个实现细节。它已经被明确规定了 - 而且必须如此,以便其行为始终可预测和可靠(一旦您理解了机制)。 - Stephen Cleary
显示剩余6条评论
5个回答

41

你还是对 async 的理解存在误解。 async 关键字并不意味着“在另一个线程上运行”。

如果要将一些代码推送到另一个线程上,你需要显式地这样做,例如使用 Task.Run

await Task.Run(() => Logger.LogInfo("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId));

我有一篇介绍async/await的文章,你可能会觉得有帮助。


Task.Run可以不被等待吗? - toha
@toha:我不明白你的问题。Task.Run是被await的。 - Stephen Cleary
我使用任务(task),而非等待(await)。但有时候任务在完成之前并不被调用,当超出函数块范围时。我希望任务一直运行,直到完成,即使主进程已经离开了函数块范围。能否实现这个需求?如果我使用等待(await)的话,会需要很长时间,所以我需要它在超出函数调用时也不会被等待和调用。 - toha
@toha:不太清楚你在问什么。你能提出自己的问题吗? - Stephen Cleary
现在的问题还是一样的。我没有等待我的异步任务完成,而是在异常块中使用它,当等待另一个函数调用时,异步任务函数并没有完全完成。我认为这与异步任务生命周期有关,当它被触发时不等待,在异常块中。您是否有相关文章可以提供,先生? - toha
显示剩余2条评论

36

如果您的Logger.LogInfo已经是异步的,那么这已经足够了:

public void PushCallAsync(CallNotificationInfo callNotificationInfo)
{
    Logger.LogInfo("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId,
}

如果不是,则异步启动它而无需等待

public void PushCallAsync(CallNotificationInfo callNotificationInfo)
{
    Task.Run(() => Logger.LogInfo("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId));
}

我读了一下,感觉更像是风格上的差异而不是实际行为上的差异,不是吗? - Visions
10
No. Task.Run使用不同的默认值:DenyChildAttachTaskScheduler.DefaultTaskScheduler特别重要,因为StartNew默认会使用TaskScheduler.Current,这会根据调用者的上下文以不同方式安排委托。这已经使许多人犯错,因此许多团队采用了代码规则,除非指定了TaskScheduler,否则不允许使用StartNew - Stephen Cleary
不知道,谢谢提供信息。将代码更改为Task.Run。 - Visions
7
按照惯例,除非一个方法有 "async" 关键字,否则不应该将其命名为 "Async"。 - Graeme Wicksted
如果您的Logger.LogInfo已经是异步的,那就足够了:>>我的方法已经是异步的,但在调试时,代码在完成之前就退出了..如何确保LogInfo始终退出直到完成,并且没有等待? - toha

6

你误解了async的含义。它实际上只是告诉编译器在后台为你执行控制流反转,以便将整个方法堆栈标记为异步。

你实际想做什么取决于你的问题。(假设你的调用Logger.LogInfo(..)是一个async方法,因为它最终会调用File.WriteAsync()等操作)。

  • 如果你的调用函数是一个void事件处理程序,那么就没问题了。异步调用将在后台某个程度上发生(即File.WriteAsync),你不需要在控制流中期望任何结果。这就是fire and forget。
  • 但是,如果你想对结果进行任何操作,或者只有在Logger.LogInfo(..)完成后才能继续,那么你必须采取预防措施。这种情况通常发生在你的方法“在调用堆栈的中间”。然后,Logger.LogInfo(..)通常会返回一个Task,你可以等待它。但要注意不要调用task.Wait(),因为它会死锁你的GUI线程。而应该使用await或返回任务(然后可以省略async):

public void PushCallAsync(CallNotificationInfo callNotificationInfo) 
{
   return Logger.LogInfo("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId); 
}

或者

 public async void PushCallAsync(CallNotificationInfo callNotificationInfo) 
 {
    await Logger.LogInfo("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId); 
 }

7
除非是事件处理程序,否则不应使用async void。这是唯一可以接受的时间和制作异步事件处理程序的唯一正确方法。如果您的同步方法是void,则应该是async Task。这是由于异常处理,在事件处理程序上将在synchronizationContext上抛出(类似于同步事件),而不是在调用方上抛出。如果可能,异步事件处理程序应该是一个薄的私有包装器,一个async Task函数(以便进行测试或重用)。 - Graeme Wicksted
@GraemeWicksted,你说得没错,我支持“不要使用async void”,但为了理解,这是我需要的 - 最终你通常会在某个地方使用async void。 - Robetto
1
我不确定我理解你的回复。这在这里如何证明?在我看来,它应该是async Task,因为它不是事件处理程序。请记住:async Task本质上是async Task<void>,但后者根本不存在。async voidasync Task之间唯一的区别是异常传播,它只对事件处理程序有意义。我没有观察到任何具体的例子表明其他情况。 - Graeme Wicksted

2
如果Logger.LogInfo是同步方法,那么整个调用将仍然是同步的。如果你只想在单独的线程中执行代码,async并不是适合这项工作的工具。尝试使用线程池代替:
ThreadPool.QueueUserWorkItem( foo => PushCallAsync(callNotificationInfo) );

1
更倾向于使用Parallel Task库,而不是直接调用线程池...这可能会导致根据平台(WinRT,Win,WP8等)具有不可预见的后果。根据我的经验。=) - J. Steen
2
你引起了我的兴趣... 你能否提供更多关于后果的细节? - ya23
好的。我已经成功地使用QueueUserWorkItem(仅使用两个线程,所以它也很敏感)在几个设备上完全卡住了CPU,而Task库将完美地管理它。我还遇到了一个问题,即我已经排队了几个线程,直到它们全部完成,回调函数才会被触发,就像是一种线程雪崩。 - J. Steen
2
如果 Logger.LogInfo 不是同步方法,难道你的意思不是如果 Logger.LogInfo 是同步方法吗? - Liam

1

根据你的例子

public async Task PushCallAsync(CallNotificationInfo callNotificationInfo)
{
    Logger.LogInfo("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId,
}
  • Logger.LogInfo同步调用
  • async关键字给方法PushCallAsync提供了等待的能力,但它从不等待任何东西

如果您的意图是让该方法异步运行 - 正如名称PushCallAsync所暗示的那样,您必须找到一种替代方式来同步调用LogInfo。

如果存在一个LogInfoAsync方法,并且试图避免使用await是不明智的。等待非常重要,因为:

  • 它捕获并抛出可能发生在任务执行过程中的异常 - 否则会丢失/未处理
  • 它通过等待结果确保执行顺序

如果您特别想要fire-and-forget行为,即不依赖于执行顺序(例如,在此情况下不关心日志消息的顺序),则调用LogInfoAsync()而不等待结果。

由于您没有使用任何await,因此不需要将方法标记为async。使其异步的不是async关键字,而是调用其他方法的异步性。

public Task PushCallAsync(CallNotificationInfo callNotificationInfo)
{
    // Fire and forget - we do not care about the result, failure, or order of this log message
    _ = Logger.LogInfoAsync("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId,
    Task.CompletedTask;
}

或非异步

public void PushCall(CallNotificationInfo callNotificationInfo)
{
    // Fire and forget - we do not care about the result, failure, or order of this log message
    _ = Logger.LogInfoAsync("Pushing new call {0} with {1} id".Fill(callNotificationInfo.CallerId,
}

不是说方法名字Push暗示了它是有序的。所以如果你真的不在乎顺序,我会给它取一个不同的名字。否则,正如Push所暗示的那样,使用await确保顺序是正确的。

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