如何在不调用 Wait() 的情况下将 RunAsync() 方法 运行 保留在后台?

How to keep the RunAsync() method running in the background without calling Wait()?

尝试从使用 BackgroundWorker 升级到 Task,我对如何在不调用 Wait() 的情况下 运行ning 保持后台作业感到困惑。

这是后台作业(已简化):

    private async Task RunAsync()
    {
        HttpResponseMessage response = await TheHttpClient.GetAsync(Path);
        if (response.IsSuccessStatusCode)
        {
            textBox_Response.Text = await response.Content.ReadAsStringAsync();
        }
    }

当发送按钮被点击时,上面的代码应该运行,但是因为后台代码有多个await异步调用,我猜有好几个Wait()需要调用:

    private void button_Send_Click(object sender, EventArgs e)
    {
        Task t = RunAsync();
        while (!t.IsCompleted)
        {
            t.Wait();
        }
    }

但这会在后台作业的整个处理时间内阻塞 GUI。

如何从按钮单击处理程序立即启动后台作业和 return,同时允许后台作业 运行 所有异步调用?

我知道如何使用 BackgroundWorker 实现此目的,我不应该在这里使用任务吗?

事件处理程序是唯一允许你做的地方async void,你所做的就是让事件处理程序异步并在那里等待它

private async void button_Send_Click(object sender, EventArgs e)
{
    await RunAsync();

    //Do code here that needs to run after RunAsync has finished.
}

很多时候人们认为async await是一个由多个线程执行的异步过程,但实际上它都是由一个线程完成的,除非你使用Task.Run或类似的功能启动一个新线程。

this interview中(在中间的某个地方。搜索async)Eric Lippert通过餐厅厨师的作品比较了async-await。如果他正在烤面包,他可以等面包做好后再煮鸡蛋,或者他可以先煮鸡蛋,然后再回到面包上。看起来厨师当时在做两件事,但实际上他只在做一件事,每当他需要等待某事时,他就开始四处看看是否可以做其他事情。

如果你的唯一线程调用一个异步函数,而不等待它。您的唯一线程开始执行该函数,直到他看到等待。如果他看到一个,他不会等到异步函数完成,而是记住他在哪里等待并在他的调用堆栈中上升以查看他的调用者是否有其他事情要做(== 没有等待)。调用者可以执行下一个语句,直到遇到等待。在这种情况下,控制权将返回调用堆栈给调用者,直到他等待等。如果您的唯一线程正在等待调用堆栈控制中的所有函数,则返回给第一个等待。完成后,您的线程开始执行 await 之后的语句,直到他遇到另一个 await,或者直到函数完成。

您可以确定每个异步函数在某处都有等待。如果没有 await 将其创建为异步函数是没有意义的。事实上,如果您忘记在异步函数中的某处等待,您的编译器会警告您。

在您的示例中,您可能会遇到错误,或者至少会收到警告,提示您忘记声明 button_send_click 异步。没有这个程序就不能等待。

但是在您将其声明为异步并按下按钮后,您的 GUI-thread 将调用 RunAsync 并进入函数 TheHttpClient.GetAsync.

这个函数里面有一个 await ReadAsStringAsync,因为 ReadAsStringAsync 被声明为 async,所以我们可以确定那个函数里面有一个 await。一旦满足此等待,控制权就会交还给您的 TheHttpClient.GetAsync。如果不等待,该函数可以自由执行下一条语句。一个例子是这样的:

private async Task RunAsync()
{
    var responseTask = TheHttpClient.GetAsync(Path);
    // because no await, your thread will do the following as soon as GetAsync encounters await:
    DoSomethingUseFul();

    // once your procedure has nothing useful to do anymore
    // or it needs the result of the responseTask it can await:
    var response = await responseTask;
    if (response.IsSuccessStatusCode)
    ...

等待在 DoSomethingUseful() 之后。因此,该功能无法继续。在 responseTask 完成之前什么都不做,而是将控制权交还给调用者:button_send_click。如果那个函数没有等待,它就可以自由地做其他事情。但是由于 button_send_click 正在等待控制权交还给调用者:您的 UI 可以自由地做其他事情。

但请记住:为您做早餐的永远是唯一的厨师您的线程一次只能做一件事

优点是您不会 运行 陷入使用多线程时遇到的困难。缺点是只要你的线程没有遇到await,就忙不过来了。

如果您需要做一些冗长的计算而您不想忙于计算,您可以启动一个单独的线程来执行此计算。这让你的线程有时间做其他事情,比如保持 UI 响应直到它需要计算结果:

private async void Button1_Clicked(object sender, ...)
{
    var taskLengthyCalculation = Task.Run( () => DoLengthyCalculation(...));
    // while a different thread is doing calculations
    // I am free to do other things:
    var taskRunAsync = RunAsync();
    // still no await, when my thread has to await in RunAsync,
    // the next statement:
    DoSomethingUseful();
    var result = await taskRunAsync();
    var resultLengthyCalculation = await taskLengthyCalculation;