我试图以最简单的形式理解异步等待。为了这个例子,我想创建一个非常简单的方法,将两个数字相加,当然,这根本不需要处理时间,只是在这里制定一个例子。
示例 1
private async Task DoWork1Async()
{
int result = 1 + 2;
}
示例 2
private async Task DoWork2Async()
{
Task.Run( () =>
{
int result = 1 + 2;
});
}
如果我等待 DoWork1Async()
,代码将同步运行还是异步运行?
我是否需要用 Task.Run
包装同步代码以使方法可等待和异步,以免阻塞 UI 线程?
我想弄清楚我的方法是 Task
还是返回 Task<T>
我需要用 Task.Run
包装代码以使其异步。
我在网上看到一些示例,人们正在等待其中没有任何异步内容且未包装在 Task.Run
或 StartNew
中的代码。
首先,让我们澄清一些术语:“异步”(async
)意味着它可能会在调用线程开始之前将控制权交还给调用线程。在 async
方法中,那些“屈服”点是 await
表达式。
这与术语“异步”非常不同,因为 MSDN 文档多年来(误用)来表示“在后台线程上执行”。
为了进一步混淆这个问题,async
与“等待”非常不同;有一些 async
方法的返回类型不是可等待的,许多方法返回的可等待类型不是 async
。
他们不是什么就够了;它们是这样的:
async 关键字允许使用异步方法(即,它允许等待表达式)。异步方法可能返回 Task、Task
任何遵循某种模式的类型都可以等待。最常见的等待类型是 Task 和 Task
因此,如果我们将您的问题重新表述为“我怎样才能以一种可等待的方式在后台线程上运行操作”,答案是使用 Task.Run
:
private Task<int> DoWorkAsync() // No async because the method does not need await
{
return Task.Run(() =>
{
return 1 + 2;
});
}
(但这种模式是一种糟糕的方法;见下文)。
但是,如果您的问题是“我如何创建一个可以返回给调用者而不是阻塞的 async
方法”,那么答案是声明方法 async
并使用 await
作为其“让步”点:
private async Task<int> GetWebPageHtmlSizeAsync()
{
var client = new HttpClient();
var html = await client.GetAsync("http://www.example.com/");
return html.Length;
}
因此,事物的基本模式是让 async
代码在其 await
表达式中依赖于“awaitables”。这些“等待对象”可以是其他 async
方法,也可以只是返回等待对象的常规方法。返回 Task
/Task<T>
的常规方法可以使用 Task.Run
在后台线程上执行代码,或者(更常见)它们可以使用 TaskCompletionSource<T>
或其快捷方式之一(TaskFactory.FromAsync
, Task.FromResult
等)。我不建议将整个方法包装在 Task.Run
中;同步方法应该具有同步签名,并且应该由消费者决定是否应该将其包装在 Task.Run
中:
private int DoWork()
{
return 1 + 2;
}
private void MoreSynchronousProcessing()
{
// Execute it directly (synchronously), since we are also a synchronous method.
var result = DoWork();
...
}
private async Task DoVariousThingsFromTheUIThreadAsync()
{
// I have a bunch of async work to do, and I am executed on the UI thread.
var result = await Task.Run(() => DoWork());
...
}
我的博客上有一个async
/await
intro;最后是一些很好的后续资源。 async
的 MSDN 文档也非常好。
使用 async 装饰方法时要记住的最重要的事情之一是至少有 一个 await<方法中的 /strong> 运算符。在您的示例中,我将使用 TaskCompletionSource 将其翻译如下所示。
private Task<int> DoWorkAsync()
{
//create a task completion source
//the type of the result value must be the same
//as the type in the returning Task
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
Task.Run(() =>
{
int result = 1 + 2;
//set the result to TaskCompletionSource
tcs.SetResult(result);
});
//return the Task
return tcs.Task;
}
private async Task DoWork()
{
int result = await DoWorkAsync();
}
async void
被认为是“不好的做法”,有很多应用程序你会使用它,基本上任何时候你需要做一些你不关心的事情什么时候结束。
当您使用 Task.Run 运行方法时,Task 从线程池中获取一个线程来运行该方法。所以从 UI 线程的角度来看,它是“异步的”,因为它不会阻塞 UI 线程。这对于桌面应用程序来说很好,因为您通常不需要很多线程来处理用户交互。
但是,对于 Web 应用程序,每个请求都由一个线程池线程提供服务,因此可以通过保存这些线程来增加活动请求的数量。频繁使用线程池线程来模拟异步操作对于 Web 应用程序是不可扩展的。
真正的异步不一定涉及使用线程进行 I/O 操作,例如文件/数据库访问等。您可以阅读本文以了解为什么 I/O 操作不需要线程。 http://blog.stephencleary.com/2013/11/there-is-no-thread.html
在您的简单示例中,这是一个纯 CPU 密集型计算,因此使用 Task.Run 就可以了。
I should NOT wrap the synchronous call in Task.Run()
是正确的。如果你这样做,你只是在切换线程。即,您正在解除对初始请求线程的阻塞,但您正在从线程池中获取另一个线程,该线程可能用于处理另一个请求。唯一的结果是当调用以绝对零增益完成时的上下文切换开销
async
方法必须返回Task
、Task<T>
或void
。Task
和Task<T>
是可等待的;void
不是。async void
方法签名将编译,这只是一个非常糟糕的想法,因为您失去了指向异步任务的指针void
不可等待。Task.Run
的调用(如此答案中的DoWorkAsync
)。使用Task.Run
从 UI 上下文调用方法是合适的(如DoVariousThingsFromTheUIThreadAsync
)。Task.Run
invoke 方法是有效的,但如果方法的所有(或几乎所有)代码周围都有一个Task.Run
,那么这是一种反模式 - 只需保持该方法同步并将Task.Run
上移一级。