使用Task.Run调用异步方法是否不正确?

12

最近我看到了一个自由承包商写的代码,在我们公司为我们工作。它可能非常巧妙,也可能很傻(我认为是后者,但我想要第二个意见)。我对asyncawait的使用不是非常精通。

基本上它的运作方式如下:

public bool Send(TemplatedMessageDto message)
{
    return Task.Run(() => SendAsync(message))
        .GetAwaiter()
        .GetResult();
}

public async Task<bool> SendAsync(TemplatedMessageDto message)
{
    //code doing stuff
    var results = await _externalresource.DothingsExternally();
    //code doing stuff
}

现在我理解第一个 Task.Run() 是没有意义和低效的?实际上应该是:

public bool Send(TemplatedMessageDto message)
{
    return SendAsync(message))
    .GetAwaiter()
    .GetResult();
}

public async Task<bool> SendAsync(TemplatedMessageDto message)
{
    //code doing stuff
    var results = await _externalresource.DothingsExternally();
    //code doing stuff
}

我还不确定这是否真的是异步方法,因为它仍然会等待,对吧?我认为即使重新编写,它唯一的优点就是释放主工作线程。

有人可以确认这个第一个任务不应该在那里吗?


1
为什么要使用 GetAwaiter().GetResult() 调用而不是 .Result?为什么要等待,而不是编写 public async Task<bool> Send ... await Task.Run(); - Panagiotis Kanavos
2
如果“其他内容”足够沉重,那么Task.Run并不低效 - async并不能使任何东西异步运行。在await之前的所有内容都将在调用线程上运行。如果需要并发运行它,应使用Task.Run,或者更好的方法是将其提取到自己的方法中,并使用Task.Run进行调用。 - Panagiotis Kanavos
@PanagiotisKanavos,是的,这个想法确实在我脑海中出现过。我认为他并没有真正理解自己在做什么。 - Liam
你真的需要同步版本吗? - Gusdor
2个回答

9
我并不认为这是一个异步方法,因为它仍然会等待,对吗?
正如Yuval所解释的那样,确实不是。您不应该在异步方法上使用同步。
就我所理解的,第一个Task.Run()是无意义和低效的?
不完全是这样,这种方式使用Task.Run仍有价值。
由于您正在阻塞异步方法(这是不应该做的),存在死锁的可能性。UI应用程序和asp.net中会出现死锁情况,因为您有一个SynchronizationContext。
使用Task.Run清除了该SynchronizationContext,因为它将工作卸载到ThreadPool线程上,并消除了死锁的风险。
因此,阻塞很糟糕,但如果最终必须阻塞,则使用Task.Run更安全。

你能解释一下关于异步方法中的“阻塞(不应该这样做)”的部分吗?我现在有点困惑了。_externalresource是一个返回Task<List<value>>的第三方库,而这个方法(我的Send()方法)只需要确认该列表是否包含值。我本来打算将其更改为同步方法并使用.Result,但我认为你是在说这是一个坏主意? - Liam
@Liam 在异步方法上进行阻塞通常是个坏主意。它会破坏性能并导致死锁。最好的做法是将整个流程变为异步。 - i3arnon
1
@Liam 如果你必须使用同步选项,最好一路使用同步调用(即 var results = _externalresource.DothingsExternally()),但如果不存在这种情况,则同步覆盖异步是不可避免的。 - i3arnon
也许是库有问题。我想我可能会发布一个后续问题。谢谢你的帮助。 - Liam
如果你有兴趣的话,我已经在stackoverflow上发布了一个后续问题。 - Liam

7

我也不确定这是否真的是一个异步方法,因为它仍然会等待,对吗?

你的承包商使用了 sync over async反模式。他可能是为了避免创建一个额外的同步执行方法而这样做的。他不必要地调用了 Task.Run 并立即在其上使用 GetResult 阻塞。

使用 GetAwaiter().GetResult() 将传播内部异常(如果发生),而不是包装的 AggregateException

我认为它唯一的优点(即使重新编写)就是释放主工作线程。

您的版本和他的版本在执行时都会阻塞主线程,而他的版本还会通过使用线程池线程来执行。正如 Bar 所提到的,这可以帮助避免与同步上下文调度相关的死锁问题。如果需要,我建议创建同步等效方法。


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