异步等待vs来自更高级别调用的结果?

时间:2018-09-29 07:02:50

标签: c# asynchronous async-await

我有这个简单的存储库方法:

public async Task<Blog> GetById(int id)
{
    return await _dbContext.Blogs.FirstOrDefaultAsync(p => p.Id == id);
}

我阅读了一些有关异步等待的答案,但我仍然不明白如何正确调用此GetById方法:

public async Task DoSomething1()
{
    var blog = await _blogRepository.GetById(1);
    Console.WriteLine(blog.Title);
}

public void DoSomething2()
{
    var blog = _blogRepository.GetById(1).Result;
    Console.WriteLine(blog.Title);
}

正确的意思是:不会像这篇文章中描述的那样阻塞线程: https://msdn.microsoft.com/en-us/magazine/dn802603.aspx?f=255&MSPPError=-2147217396

我个人认为,在这种情况下,正确的方法是DoSomething2。 因为线程阻塞是在FirstOrDefaultAsync运行时发生的,所以这就是为什么我在GetById方法中使用async和await的原因,所以它真的需要在更高的方法(如DoSomething1)中使用一个async await吗?在DoSomething2这样的情况下可以使用Result吗?

选择的优缺点是什么?

(我使用.NET 4.5)

5 个答案:

答案 0 :(得分:1)

DoSomething1()会避免阻塞,而不是DoSomething2()

您使用async修饰符来指定方法,lambda表达式或匿名方法为asynchronous

异步方法将同步运行,直到到达其第一个await表达式为止,此时该方法将被挂起,直到等待的任务完成为止,在这种情况下,该方法就是GetById异步方法。


您的代码中也有错误等待发生...

public async Task DoSomething1()
{
    var blog = await _blogRepository.GetById(1);
    Console.WriteLine(blog.Title); // this line has the bug
}

在搜索基础数据库时,GetById方法在内部使用FirstOrDefault,因此,如果您搜索不存在的博客,则该方法将返回空对象。尝试访问该对象的Title属性,但由于其null ...,您将获得null引用异常。尝试访问其属性之前,请检查该对象是否为空

答案 1 :(得分:1)

Task.Result调用将阻塞调用线程,直到操作完成。

这是一篇很棒的文章,很好地解释了它 https://montemagno.com/c-sharp-developers-stop-calling-dot-result/

答案 2 :(得分:1)

关于第一个实现(DoSomething1),您可以在此处阅读: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/await

  

await表达式不会阻塞正在其上执行的线程。而是,它使编译器将async方法的其余部分注册为等待任务的延续。然后,控制权返回给异步方法的调用方。任务完成后,它将调用其继续,并且异步方法的执行将从中断处继续执行。

关于第二个(DoSomething2) 根据{{​​3}}

  

访问属性的get访问器会阻塞调用线程,直到异步操作完成为止;等效于调用Wait方法。

     

一旦操作结果可用,就将其存储并在随后对Result属性的调用中立即返回。请注意,如果在任务操作期间发生异常,或者任务已被取消,则Result属性不会返回值。

要证明这一点,您可以在执行之前和之后检查线程ID。例如

var threadId1 = Thread.CurrentThread.ManagedThreadId;
var blogPost = await DoSomething1();
var threadId2 = Thread.CurrentThread.ManagedThreadId;
var blogPost = DoSomething2().Result;
var threadId3 = Thread.CurrentThread.ManagedThreadId;

如果输出线程ID,则结果中的threadId2和threadId3将始终相同,因此不会发生线程更改,并且线程被阻塞。

{
  "threadId1": 30,
  "threadId2": 15,
  "threadId3": 15
}

答案 3 :(得分:0)

我知道这是一个有争议的话题,我认为这值得引起争议,所以就这样:

首先,请让我明确指出DoSomething2是一种阻止方法;这意味着它将在调用线程上运行并阻止调用线程,而DoSomething1不会阻止调用线程,而是在线程池线程上运行。

现在,根据我对异步方法的了解,它们的唯一目的是允许并行处理,从而最大限度地利用CPU。从桌面开发人员的角度来看,这意味着,当您调用异步方法时,您可以具有响应式UI,并且可以在将工作卸载到非主线程时更新UI。

如果您的应用程序旨在并行运行工作单元,那么对我来说,使用DoSomething1而不是DoSomething2是有意义的,因为这样可以最大程度地利用CPU,但是如果您的应用程序设计为按顺序运行工作单元(就像我相信您在控制台应用程序中所做的那样),使用DoSomething1不会带来任何好处,因为一次只能运行一个操作而我会改用DoSomething2

话虽如此,您可以在控制台应用程序中使用AsyncContext.Run(() => MyMethodAsync(args));在单独的上下文中运行异步方法。

总而言之,除非您的控制台应用程序在线程上进行一些并行工作,否则我会说使用DoSomething2会很好。

P.S,您应该修复@AydinAdn提到的错误。

答案 4 :(得分:0)

如果将await应用于返回Task Result的方法调用的结果,则await表达式的类型为Result。如果将await应用于返回Task的方法调用的结果,则await表达式的类型为空