我想知道为什么我们需要 C# 中的异步函数。我做了研究,如果有一个 IO 密集型任务想要等待而不阻塞,看起来会使用异步函数。
public class HelloWorld
{
public static async Task DoIO()
{
await Task.Delay(1000, new CancellationTokenSource().Token);
Console.WriteLine("DONE");
}
public static void Main(string[] args)
{
DoIO();
Console.WriteLine("Start");
while (true);
// Output: Start \n DONE
}
}
另一种情况是当一个CPU密集型任务要启动,但我们不想等待它时。它应该在后台运行。它看起来与之前的代码块一模一样,但将
DoIO()
替换为 DoCPU()
,这是一个需要大量计算的函数。
public static async Task DoCPU()
{
await Task.Run(() => {
// Heavy computation
});
}
在这两种情况下,关键字都是“无需等待”。我们希望调用异步方法而不阻塞调用者方法。
现在这就是我的问题出现的地方:为什么我会收到警告“因为不等待此调用,所以在调用完成之前继续执行当前方法。考虑将“await”运算符应用于调用?” 异步函数的全部目的不就是 NOT wait 吗?否则为什么不直接使用默认阻塞的“常规”同步方法呢?
异步函数的全部目的不就是不等待吗?
等待和等待是不同的事情。在 .Net 的上下文中,
Wait
是阻塞线程的同步操作。而 await
是异步的,让线程做其他事情。 async/await 的魔力让您能够以与同步代码几乎相同的方式编写异步代码。
考虑两个等效的程序:
public static async Task DoIO()
{
await Task.Delay(1000);
Console.WriteLine("End");
}
public static async Task Main(string[] args)
{
Console.WriteLine("Start");
await DoIO();
}
和
public static void DoIO()
{
Thread.Sleep(1000);
Console.WriteLine("End");
}
public static void Main(string[] args)
{
Console.WriteLine("Start");
DoIO();
}
对于控制台程序来说,这些是直接等效的。使用同步变体是完全可以的。
如果我们从异步变体中删除 Main 方法中的
await
,则程序不再等效。程序将打印“start”,创建一个 1 秒后计时的计时器,然后在主线程到达 Main 方法末尾时退出。从不打印“结束”。这就是您收到警告的原因,不等待任务是一个很容易犯的错误,而且通常是一个错误。
如果这是一个 UI 程序,那就会有区别。如果您在 UI 线程上使用
Thread.Sleep
或任何其他阻塞操作,您的 UI 将冻结,并且很快窗口将显示“应用程序无响应”消息。 Async/await 可以轻松地执行缓慢的操作,同时仍然保持 UI 响应,并且仍然确保只有 UI 线程更新 UI。
异步编程的另一个常见用途是服务器,您可能有数千个并发请求都在等待数据库。对每个请求使用一个线程会浪费内存。如果您使用 async/await,则在实际使用它来计算某些内容时只需要一个线程。
您还可以使用 async/await 并发执行多个 IO 操作,通过启动多个操作,并且仅在所有操作启动后才执行等待。但执行此操作时需要小心一点,因为可能会导致各种争用问题,从而导致降低性能。
忽略评论,我会回答这个:
现在这就是我的问题所在:为什么我会收到警告“因为未等待此调用,因此在调用完成之前继续执行当前方法。考虑将“await”运算符应用于调用结果吗? ”异步函数的全部目的不就是不等待吗?否则为什么不直接使用默认阻塞的“常规”同步方法呢?
你似乎有很多误解。
首先,您不是等待方法调用,而是等待它返回的任务。
换句话说:您不必必须这样做:
var result = await DoSomethingAsync();
你完全可以做到这一点:
var task = DoSomethingAsync();
// do something else
var result = await task;
您的代码不会执行任何这些操作。因此,您会收到有关该事实的警告。
我希望第二个片段已经向您展示了为什么
await
ing a Task
与阻塞完全不同。您无法defer解开同步阻塞方法的上下文。换句话说:您无法像上面所示拆分同步调用。
在这方面,它是
Task<T>
还是 Task
也并不重要。唯一的区别是后者缺少结果值。
还要考虑到,当程序退出时,永远不会等待的“fire&forget”异步任务可能仍在运行/等待 IO,并带来随之而来的所有副作用。
有时,这是故意的,并且已知没有严重的副作用。在这种情况下,您可以随意抑制警告。