我有这个简单的存储库方法:
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)
答案 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表达式的类型为空