c# async await 让我感到困惑,似乎什么都不做

c# async await confuses me, seems to do nothing

编辑:将 LongTask() 更新为 await Task.Delay(2000); 使其按我的需要工作,所以感谢大家的回答!


我正在尝试从同步函数中调用异步函数,让同步函数在异步函数完成之前继续运行。我不知道为什么下面的代码在继续之前等待 LongTask() 完成。我认为只有当我 await LongTask() 时它才应该这样做。有人可以帮助我了解我所缺少的吗?

C#fiddlehere

using System;
using System.Diagnostics;
using System.Threading.Tasks;
                    
public class Program
{
    public static void Main()
    {
        var timer = new Stopwatch();
        timer.Start();
        LongTask();
        timer.Stop();
        
        long ms = timer.ElapsedMilliseconds;

        Console.WriteLine(ms.ToString());
    }
    
    async static void LongTask()
    {
        Task.Delay(2000).Wait();
    }
}

如评论中所述:

你必须使用 await Task.Delay(2000) 告诉编译器你想要 运行 Task.Delay(2000) 异步。 如果您仔细查看您提供的 example,编译器会显示警告:

This async methods lacks 'await'operators and will run synchronously [...]

这是 async/await

的简短介绍

你知道当你玩一些上帝类型的游戏时,例如让你的定居者建造大量建筑物,或削减 trees/making 食物等,然后你除了坐着看着他们完成之外几乎没有别的事可做,然后你可以重新开始,让建筑物大量生产骑士或弹射器或什么?

这有点浪费生命,你坐在那里除了等待某件事完成之外什么都不做,然后你可以继续做其他事情..

作为一个人,你可能不会那样做;你去冲杯咖啡,打电话给朋友,打沙袋,甚至去玩 space 探索游戏,设置任何飞船的自动驾驶仪,引导你前往某个恒星系统,进行一次需要 5 小时的旅程等待和观看星星的分钟过去了..自动驾驶仪设置,咖啡制作,建筑物仍未完成,对沙袋感到厌烦..所以你去清理..

这不是并行处理;你一边搅拌咖啡,一边在 phone 上聊天,并不是在用布擦地板,而是在充分利用你的时间,通过将大量精力倾注到一件事上来破解待办事项列表,获得尽你所能,然后切换到下一件事。你一次做一件事,但当你陷入困境时,你会转向另一件事。如果你坐下来等待一切,你需要 10 个副本才能完成 10 stop/start 个工作

async/await 是一回事。它需要同步代码——在一个长操作中从头到尾完成的事情,这是你线程关注的唯一焦点,即使它中间有 5 分钟 Wait() 它实际上坐在那里什么都不做5 分钟 - 并将其分成一堆单独的方法,这些方法由一个进程控制,该进程允许线程从它停止的地方继续

标记方法 async 是 C# 的指示器,允许编译器无形中将方​​法切割成可以放下并在需要时拾取的部分

await 是您在说“我已经到了需要完成一些操作才能继续进行此操作的地步;去寻找其他事情要做”的指标。当它遇到 await 你的线程将停止处理你的代码,因为它被告知它在后台操作完成之前什么都不做(做什么工作并不重要,让我们想象它是一些后台线程)和你的线程将去做其他事情,直到结果准备好。这确实简化了很多与编程相关的事情,因为你只处理一个线程——它的代码表现得像同步代码,但在它卡住时会去做其他事情而不是什么都不做

作为关键字 await 必须跟在 awaitable 之后,通常是 TaskTask<T> - 它代表已经设置为发生的操作。当操作完成并且线程 returns await 也有助于解开控制此进程的任务对象,并为您提供结果..

如果你对此一无所获,至少要学习:

  • 如果您想在其中使用 await,您 必须 将方法标记为 async。您通常应该将 sync 方法的 return 类型设为任务(如果 return 没有值)或 Task<X> (如果 return 有值X 型)。您还应该将该方法称为以 ..Async
  • 结尾的名称
  • 当您启动一些异步操作(您调用一些称为 ..Async 的方法)时,您在 return 中获得一个 Task<X> 对象,使用 await它得到你真正想要的X
  • 制作一个异步方法意味着调用它的方法也必须是异步的,并且调用那个的方法也必须是异步的..和..一路爬上树,跳出你的代码。这就是你的线程在必须寻找其他事情要做时如何“逃脱” - 它需要从你的代码中逃脱,所以你通过声明“一直异步”来为它制定一条路线

我在评论中说过不要使用控制台应用程序。假设您有一个 Windows 表单应用程序。 Win Forms 应用程序通常有一个线程来完成所有事情。一个线程绘制 UI,当您单击一个按钮时,它出现并且 运行 是您放入 button_Click() 处理程序中的所有代码。当它这样做时 它并没有完成绘制 UI 的正常工作。您可以将 statusLabel.Text = "Downloading data.." 设置为该方法的第一行,您可以启动一个 1 gig 文件的下载,然后您可以将状态标签设置为 "Finished " + downloadedFile.Length。您单击该按钮,但什么也看不到 - 您的应用程序的 UI 在文件下载时卡住了 30 秒。然后突然在标签里说“Finished 1024000000”:

void button_Click(object sender, EventArgs e){
  statusLabel.Text = "Starting download";
  string downloadedFile = new SomeHttpDownloadStringThing("http://..");
  statusLabel.Text = "Finished " + downloadedFile.Length;
}

为什么?嗯.. 绘制 UI 并且会在屏幕上绘制标签像素的线程被重新一直忙于下载文件。它进入那个 SomeHttpDownloadStringThing 方法并且在 30 秒内没有出来,然后它出来,再次设置状态标签文本,然后它离开你的代码并回到它通常居住的任何地方。它画了屏幕上的像素“完成...”。它甚至不知道它必须绘制“开始下载”。在它再次拿起画笔之前,数据已经被覆盖

让我们把它变成异步的:

async void button_Click(object sender, EventArgs e){
  statusLabel.Text = "Starting download";
  string downloadedFile = await SomeHttpDownloadStringThingAsync("http://..");
  statusLabel.Text = "Finished " + downloadedFile.Length;
}

SomeHttpDownloadStringThing return 编辑了 stringSomeHttpDownloadStringThingAsync return 编辑了 Task<string>。当 UI 线程命中那个 await 想象它告诉一些后台线程去下载文件,暂停这个方法,然后 returns 退出并返回绘制 UI。它将标签绘制为“Starting ...”,这样您就可以看到它,然后可能会找到其他一些事情要做。如果你有一个计时器来计算下载的时间,那么你的计时器也会很好地增加 - 因为 UI 线程可以自由执行它,它不会等待 Windows 执行下载而它除了等待什么都不做。

下载完成后,线程会被回调到原来的位置,并从那里开始。它不会再次 运行 第一个代码,它不会再次将状态标签设置为“开始”。

字面上是从SomeHttpDownloadStringThingAsync部分开始的,就好像它从未离开过一样,awaitTask<string>转换为下载文件的string,然后UI 线程可以设置“完成..”文本

整个事情就像同步版本一样,除了中间的一小部分 UI 线程被允许返回到其在屏幕上绘制像素和处理其他用户交互的常规工作

这就是为什么我说控制台应用程序很难理解 async/await 因为它们的 UI 在等待进行时显然不会做任何其他事情,除非你已经安排了一些事情发生,但那是更多的工作。 UI 应用程序会自动进行其他操作,如果您阻塞执行这些操作的线程,它们就会冻结,并变为“无响应”

如果你有敏锐的眼光,你会在处理程序上发现 async void 即使我说“如果你有一个 void 方法,让它成为 return 任务当你让它异步时”——winforms 事件处理程序在这方面有点特殊,你不能让它们 async Task 但它们是一个例外而不是规则。现在,经验法则 - 避免 async void

这有望更好地展示它:

static async Task Main(string[] args)
{
    var timer = Stopwatch.StartNew();
    var t1 = LongTask();
    var t2 = LongTask();
    Console.WriteLine("Started both tasks in {0}", timer.Elapsed);
    await Task.WhenAll(t1, t2);
    timer.Stop();
    Console.WriteLine("Tasks finished in {0}", timer.Elapsed);
}

async static Task LongTask()
{
    await Task.Delay(2000);
}

注意 return 类型函数的变化。还要注意输出。两个任务都在并行等待某些东西(在这种情况下延迟到期),同时,Main 函数能够继续 运行ning,打印文本,然后等待那些结果函数。

重要的是要了解异步函数 运行 的执行是同步的,直到它遇到 await 运算符。如果您在异步函数中没有任何 await 它不会异步 运行 (并且编译器应该警告它)。这是递归应用的(如果您有多个嵌套等待),直到您遇到实际要等待的东西。参见示例:

static async Task Main(string[] args)
{
    var timer = Stopwatch.StartNew();
    var t1 = LongTask(1);
    Console.WriteLine("t1 started in {0}", timer.Elapsed);
    var t2 = LongTask(2);
    Console.WriteLine("Started both tasks in {0}", timer.Elapsed);
    await Task.WhenAll(t1, t2);
    timer.Stop();
    Console.WriteLine("Tasks finished in {0}", timer.Elapsed);
}

async static Task LongTask(int id)
{
    Console.WriteLine("[{0}] Before subtask 1", id);
    await LongSubTask1();
    Console.WriteLine("[{0}] Before subtask 2", id);
    await LongSubTask2();
    Console.WriteLine("[{0}] After subtask 1", id);
}

async static Task LongSubTask1(int id)
{
    Console.WriteLine("[{0}] LongSubTask1", id);
    await Task.Delay(1000);
}
async static Task LongSubTask2(int id)
{
    Console.WriteLine("[{0}] LongSubTask2", id);
    await Task.Delay(1000);
}

如果你 运行 它,你会看到执行 运行 一直同步到 Task.Delay 调用,然后才 return 所有回到 Main 函数。

为了从 async/await 中受益,您必须一直使用它直到它最终调用一些 I/O(网络、磁盘等),您需要等待for(或其他一些不需要不断轮询才能弄清楚的事件,或者人为创建的事件,例如该样本中的延迟)。如果你需要做一些昂贵的计算(其中没有任何异步)而不占用一些特定的线程(比如,GUI 应用程序中的 UI 线程),你需要显式地开始新的用它来完成任务,稍后你可以 await 得到它的结果。