这些可等待方法有什么区别?

6

我正在研究C#中的异步编程,并想知道这些函数之间的区别,它们都执行完全相同的操作并且都是可等待的。

public Task<Bar> GetBar(string fooId)
{
    return Task.Run(() =>
    {
        var fooService = new FooService();
        var bar = fooService.GetBar(fooId);
        return bar;
    });
}

public Task<Bar> GetBar(string fooId)
{
    var fooService = new FooService();
    var bar = fooService.GetBar(fooId);
    return Task.FromResult(bar)
}

public async Task<Bar> GetBar(string fooId)
{
    return await Task.Run(() =>
    {
        var fooService = new FooService();
        var bar = fooService.GetBar(fooId);
        return bar;
    });
}

我猜第一种方式是正确的做法,直到你试图从返回的任务中获取结果之前,代码不会被执行。
在第二种方式中,代码在调用时执行,结果存储在返回的任务中。
第三种方式有点像第二种?代码在调用时执行,并返回Task.Run的结果?在这种情况下,这个函数有点愚蠢吗?
我是对还是错呢?

1
第一个是异步运行的,第二个不是,并且会阻塞。结果以同步方式返回,包装在任务中。第三个与第一个几乎相同 - async/await 只是使等待更容易,它并没有使任何东西异步化。第三个实际上有一些不必要的冗余,因为函数内部实际上并没有使用结果。 - Panagiotis Kanavos
请注意,我并不会将这个问题发布为答案,因为有很多类似的问题。 - Panagiotis Kanavos
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Marc Gravell
好的,感谢您的回答。那么,如果我要使用异步方式,第一种选项就是唯一的选项了吗? - Gralov
@Gralov 嗯,你可以看看是否可以将GetBar作为真正的异步GetBarAsync公开,而不使用底层的Task.Run。这将是另一种选择。 - Marc Gravell
2个回答

6
这些方法的实现都没有意义。你所做的只是将阻塞工作推到线程池上(或更糟的是,同步运行并将结果包装在一个Task<Bar>实例中)。相反,你应该公开同步API,并让调用者决定如何调用它。他们是否想使用Task.Run取决于他们自己。
话虽如此,这里有些不同之处: #1 第一种变体(直接通过Task.Run返回一个Task<Bar>)即使从API角度来看不太有意义,但它是最“纯粹”的。你允许Task.Run在线程池上安排给定的任务,并返回表示异步操作完成的Task<Bar>给调用方。 #2 第二个方法(使用Task.FromResult)不是异步的。它像普通的方法调用一样同步执行。结果只是简单地包装在已完成的Task<Bar>实例中。 #3 这是第一种方式的更复杂版本。你正在实现类似于#1的结果,但多了一个不必要的、甚至有点危险的await。这值得更详细地研究。 async/await非常适合通过将多个表示异步工作的Task组合成一个单元(Task)来链接异步操作。它使你能够按照正确的顺序进行操作,提供了丰富的控制流来管理异步操作,并确保在正确的线程上执行操作。 然而,在你的场景中,以上内容都没有任何好处,因为你只有一个Task。因此,没有必要让编译器为你生成状态机,只是为了完成Task.Run已经做到的事情。 一个设计不良的async方法也可能会有危险。如果你未在你的awaitedTask上使用ConfigureAwait(false),则无意中引入了SynchronizationContext捕获,会影响性能并为没有好处引入死锁风险。 如果调用方决定在具有SynchronizationContext(如Win Forms、WPF和可能ASP.NET)的环境中阻塞你的Task<Bar>(如: GetBar(fooId).Wait()GetBar(fooId).Result),他们将因为这些原因导致死锁,详见此处

这只是一个例子,更多的代码可能会被包装在Task.Run()中执行。如果我正确理解了你所提到的死锁文章,它只会在我用以下方式时出现死锁: var bar = GetBar(fooId); bar.Result; 如果我在异步方法中使用以下方式: var bar = await GetBar(fooId); 它就不会发生死锁了? - Gralov
1
@Gralov,正确的做法是等待方法1和3产生的Task<Bar>。虽然技术上也可以等待方法2产生的Task<Bar>,但这样做没有任何意义,因为大部分代码仍将同步执行,即使使用了await(所以你会得到所有与async相关的性能开销,但没有任何好处)。 - Kirill Shlenskiy

0

我在Stackoverflow的评论中读到了以下类比。由于它是在评论中,所以我无法轻易找到它,因此没有链接。

假设你要做早餐。你煮一些鸡蛋并烤一些面包。

如果你开始煮鸡蛋,那么在子程序“煮鸡蛋”中,你必须等待鸡蛋煮熟

同步将是在启动子程序“烤面包”之前等待鸡蛋煮熟。

然而,如果在煮鸡蛋的同时,你不等待,而是开始烤面包。然后你等待其中任何一个完成,并继续进行“煮鸡蛋”或“烤面包”的过程,以先完成的为准。这是异步的但不是并发的。仍然是一个人在做所有事情。

第三种方法是雇用一个厨师来煮鸡蛋,而你则烤面包。这真正是并发的:两个人在做事情。如果你真的很富有,你也可以雇一个烤面包机,当你看报纸时,但嘿,我们并不都生活在唐顿庄园。

回到你的问题。

第二个:同步的:主线程完成所有工作。在调用者可以做其他事情之前,该线程在鸡蛋煮熟后返回。

Nr 1 没有声明为异步。这意味着,尽管您启动了另一个线程来执行工作,但您的调用者无法继续执行其他操作,尽管您可以这样做,但您只是等待直到鸡蛋煮熟。

第三个过程被声明为异步。这意味着,一旦等待鸡蛋开始,您的调用者就可以做其他事情,比如烤面包。请注意,所有工作都由一个线程完成。

如果您的调用者不做任何事情,只是等待您,那么它将没有太大用处,除非您的调用者也被声明为异步。这将为调用者的调用者提供执行其他操作的机会。

通常在正确使用async-await时,您会看到以下内容: - 每个声明为async的函数返回Task而不是void,并且返回Task<TResult>而不是TResult - 只有一个例外:事件处理程序返回void而不是Task。 - 调用异步函数的每个函数都应该声明为async,否则使用async-await就没有真正的用处。 - 在调用异步方法后,您可以开始烤面包,同时煮鸡蛋。当面包烤好后,您可以等待鸡蛋,或者在烤面包期间检查鸡蛋是否准备好,或者最有效的方式可能是等待Task.WhenAny,以继续完成鸡蛋或面包,或者在没有任何有用事情可做的情况下等待Task.WhenAll,只要两者中有一个未完成即可。
希望这个比喻有所帮助。

#1 描述是错误的。该变体在将 Task<Bar> 返回给调用者之前,在“主”线程上同步执行最少量的工作(此时“主”线程可以做其他事情)。在#3中,保证同步运行的方法部分略微更大:您仍将在“主”线程上发生对Task.Run的调用,但在最终将Task<Bar>返回给调用者之前,还会跟随所有可等待相关的工作(GetAwaiter,检查IsCompleted,安排继续),也在“主”线程上执行。 - Kirill Shlenskiy
早餐的比喻暗示了引入多线程,而这并不是async-await的(主要)目的。 - MEMark

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