【问题标题】:Practical example of async await does not meet expectationsasync await 不符合预期的实际例子
【发布时间】:2019-10-26 19:45:30
【问题描述】:

与本题相关:Does await completely blocks the thread?

[...] 它将首先检查被调用的方法是否完成,如果没有完成,将注册延续并从该方法调用返回。稍后,一旦该方法完成,它将重新进入状态机以完成该方法

还有这个问题:When is the best place to use Task.Result instead of awaiting Task

await 仅表示“在此任务完成之前,此工作流程无法继续进行,因此如果未完成,找到更多工作要做,稍后再回来

最后是这篇文章:https://blog.stephencleary.com/2012/02/async-and-await.html

如果“await”看到可等待对象尚未完成,则它会异步执行。它告诉 awaitable 在完成时运行该方法的其余部分,然后从 async 方法返回。 稍后,当 awaitable 完成时,它将执行 async 方法的剩余部分。如果您正在等待内置的可等待对象(例如任务),则异步方法的其余部分将在“等待”返回之前捕获的“上下文”上执行。。 em>

因此,从这些帖子中,我了解到 await 运算符确实没有阻塞,但是当我尝试对其进行测试时,我就是无法让这个原理按照它所说的方式工作。显然我错过了一些东西:

    //This will take 10 seconds
    [HttpGet("test1")]
    public async Task<TimeSpan> test()
    {
        var t1 = DateTime.Now;

        var wait1 = DoAsyncEcho("The first!", 10000);
        var wait2 = DoAsyncEcho("The second!", 10000);


        _logger.LogInformation(await wait1);
        _logger.LogInformation(await wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }

    //This will take 10 seconds too
    [HttpGet("test2")]
    public async Task<TimeSpan> test2()
    {
        var t1 = DateTime.Now;

        var wait1 = DoAsyncEcho("The first!", 10000);
        var wait2 = DoAsyncEcho("The second!", 10000);

        Thread.Sleep(10000);

        _logger.LogInformation(await wait1);
        _logger.LogInformation(await wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }


    //This will take 20
    [HttpGet("test3")]
    public async Task<TimeSpan> test3()
    {
        var t1 = DateTime.Now;

        var wait1 = await DoAsyncEcho("The first!", 10000);
        var wait2 = await DoAsyncEcho("The second!", 10000);

        _logger.LogInformation(wait1);
        _logger.LogInformation(wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }



    //This will take 30
    [HttpGet("test4")]
    public async Task<TimeSpan> test4()
    {
        var t1 = DateTime.Now;

        var wait1 = await DoAsyncEcho("The first!", 10000);
        var wait2 = await DoAsyncEcho("The second!", 10000);

        Thread.Sleep(10000);

        _logger.LogInformation(wait1);
        _logger.LogInformation(wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }

    private Task<string> DoAsyncEcho(string v, int t)
    {
        return Task<string>.Factory.StartNew(() =>
            {
                Thread.Sleep(t);
                return v;
            }
        );
    }

正如我从 test3test4 方法中看到的,await 确实在等待,它不会进入状态机并稍后进行回调,因为它等待第一个 @ 的整整 10 秒987654328@,然后在第二次通话中再打 10 秒。在 test1test2 方法上,执行时间持续 10 秒,因为代码不会等待 DoAsyncEcho 的返回,而是稍后等待结果。特别是 test2 方法并行休眠 10 秒的 3 次调用,所以毕竟它只是一个 10 秒的运行。

我在这里缺少什么?

【问题讨论】:

  • What do I'm missing here? 你期望什么结果('nt)?为什么?你的时间安排在我看来是合理的。
  • 由于发布的问题和我期待等待不等待的文章,是直接写在异步调用上还是稍后在使用值时写。我希望它进入所谓的状态机,它会记住调用的状态,并且仅在绝对需要等待结果值时才等待它们。 tl;博士;让所有方法运行相同的时间
  • await X(); 将等待任务完成,然后继续执行下一条语句。这就是你所看到的。
  • 异步等待,它不会阻塞线程。
  • 在第一次测试中,您几乎同时开始任务,因此它们也几乎同时完成(大约 10 秒)。在 test2 中它是一样的,但是你睡了 10 秒(在任务完成之前)。在此睡眠时间之后,任务将完成,这就是为什么时间也在 10 秒左右。在 test3 中,您等到第一个任务完成,然后等到 2d 任务完成,这就是为什么它是 20 秒。在 test4 中,睡眠时间相同加 10 秒

标签: c# asynchronous async-await


【解决方案1】:

我认为证明这一点的最佳方式是通过一个简单的 Windows 窗体应用程序。

创建一个默认的 Windows 窗体应用并在其上放置 3 个按钮(称为 button1button2button3

然后添加以下代码:

async void button1_Click(object sender, EventArgs e)
{
    this.Text = "[button1_Click] About to await slowMethodAsync()";
    int result = await slowMethodAsync();
    this.Text = "[button1_Click] slowMethodAsync() returned " + result;
}

void button2_Click(object sender, EventArgs e)
{
    this.Text = "[button2_Click] About to start task to call slowMethod()";
    int result = 0;

    Task.Run(() =>
    {
        result = slowMethod();
    }).ContinueWith(_ =>
    {
        this.Invoke(new Action(() =>
        {
            this.Text = "[button2_Click] slowMethod() returned " + result;
        }));
    });
}

void button3_Click(object sender, EventArgs e)
{
    this.Text = "[button3_Click] About to call slowMethod()";
    int result = slowMethod();
    this.Text = "[button3_Click] slowMethod() returned " + result;
}

static async Task<int> slowMethodAsync()
{
    await Task.Delay(5000);
    return 42;
}

static int slowMethod()
{
    Thread.Sleep(5000);
    return 42;
}

如果您尝试使用此代码,您会注意到以下内容:

按下按钮1会立即将标题更改为[button1_Click] About to await Task.Delay(5000),您可以在等待5秒后调整对话框大小,然后标题将更改为[button1_Click] Awaited Task.Delay(5000)

处理按钮 2 的代码非常粗略地等同于从按钮 1 的 await 代码生成的状态机。如果你按下button2,你会看到类似于按下button1的效果。

(await 的实际代码实际上是完全不同的,但是使用延续的底层机制——即ContinueWith()Invoke(),在 UI 线程上继续执行 await 之后的代码说明了它的方法。)

button3 的代码在Thread.Sleep() 期间完全阻塞,如果按下 button3,UI 将完全锁定 5 秒。


为了说明非 UI 示例会发生什么,请考虑以下控制台应用程序:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    static class Program
    {
        static async Task Main()
        {
            Console.WriteLine("Main thread ID = " + Thread.CurrentThread.ManagedThreadId);

            int result = slowMethod();

            Console.WriteLine("result = " + result);
            Console.WriteLine("After calling slowMethod(), thread ID = " + Thread.CurrentThread.ManagedThreadId);

            result = await slowMethodAsync();
            Console.WriteLine("result = " + result);
            Console.WriteLine("After calling slowMethodAsync(), thread ID = " + Thread.CurrentThread.ManagedThreadId);
        }

        static async Task<int> slowMethodAsync()
        {
            await Task.Delay(5000);
            return 42;
        }

        static int slowMethod()
        {
            Thread.Sleep(5000);
            return 42;
        }
    }
}

如果你运行它,你会看到类似下面的输出:

Main thread ID = 1
result = 42
After calling slowMethod(), thread ID = 1
result = 42
After calling slowMethodAsync(), thread ID = 4

注意代码是如何在等待之后在不同的线程上恢复的。

要意识到的关键是,就调用代码而言,y = await X(); 直到它有要返回的值才返回,之后运行的代码可能在不同的线程上运行。

在阻塞 THREADS 方面的效果是调用线程被释放出来执行一些其他代码,并且只有在 async 方法返回时才需要另一个线程。

在许多情况下,这意味着不需要额外的线程(用于继续),并且在所有情况下,这意味着原始调用线程不会被阻塞,可以释放到线程池中以用于其他任务。

这是所有这一切的“非阻塞”部分。

要详细了解为什么有时不需要额外的线程,请阅读 Stephen Cleary 的优秀 "There is no thread"

【讨论】:

  • 完美解释。我通过您的示例得到了它,但一切都是无效的,因此代码不必对返回的值做任何事情,因为没有返回值。在我的示例中,await 确实必须对返回的值做一些事情,在这种情况下它确实会阻塞,所以我看不到状态机,因为调用代码阻塞了时间。
  • @JJCV 我已经修改了代码,使它 awaits 可以返回一个值,但实际上并没有太大的不同。
  • 感谢您的编辑。所以“非阻塞”部分实际上不是指代码不能执行更多行,而是指要释放的线程来执行任何其他行?这在 netcore api 项目(这是我的场景)中,将等效于执行等待被释放以执行另一个传入调用的线程。这是正确的吗?
  • @JJCV 是的,这就是它的要点!实际上,它更适用于不喜欢阻塞调用线程的 ASP.Net 和基于 UI 的应用程序。
  • 解释得很漂亮!谢谢!
【解决方案2】:

您似乎混淆了等待和阻塞的两种不同解释。异步代码的目标是阻塞你的代码,而线程保持不阻塞。如果您不想阻止您的代码,那么解决方案很简单:不要使用await。但是如果你不阻塞你的代码,那么你就不能使用异步操作的结果,因为异步操作是和你的代码同时运行的。

尚未发生的事情属于未来,未来是未知的。你不仅不知道结果,甚至不知道操作是成功还是失败。在大多数情况下,这是有问题的。在继续处理此结果之前,您需要操作结果。所以你必须阻止你的代码。这就是为什么 await 被发明出来的原因,它可以在不阻塞线程的情况下阻塞你的代码。

需要线程保持畅通,以便它继续运行 UI 消息泵,使您的应用程序保持响应。仅仅因为您的 code 被阻止,您的 _application) 也不需要被阻止。对于 ASP.NET 应用程序,您需要线程保持畅通,以便它可以服务其他传入的 Web 请求。您阻塞的线程越少,您可以服务的请求就越多。在这种情况下,async/await 成为可扩展性的助推器。

【讨论】:

    猜你喜欢
    • 2019-12-04
    • 2020-03-26
    • 1970-01-01
    • 2019-09-25
    • 1970-01-01
    • 2016-01-21
    • 2019-11-19
    • 2022-01-24
    • 2021-03-11
    相关资源
    最近更新 更多