【问题标题】:async await: is the main thread suspended?异步等待:主线程是否挂起?
【发布时间】:2016-10-06 15:44:13
【问题描述】:

我正在阅读有关 async/await 关键字的信息,并且我已经阅读过:

当逻辑流到达等待令牌时,调用线程是 暂停直到通话完成。

好吧,我创建了一个简单的windows forms application,放置了两个标签、一个按钮和一个文本框,然后我编写了代码:

        private async void button1_Click(object sender, EventArgs e)
        {
            label1.Text = Thread.CurrentThread.ThreadState.ToString();
            button1.Text =  await DoWork();
            label2.Text = Thread.CurrentThread.ThreadState.ToString();
        }

        private Task<string> DoWork()
        {
            return Task.Run(() => {
                Thread.Sleep(10000);
                return "done with work";
            });            
        }

我不明白的是,当我点击按钮时,label1 会有文字 Running 并且标签只有在 10 秒后才会有相同的文字,但是在这 10 秒内我能够输入我的文本框中的文本,所以似乎主线程正在运行......

那么,async/await 是如何工作的?

这是书中的“截图”:

问候

【问题讨论】:

  • @Noseratio 我在 Andrew Troelsen 的 Pro C# 5.0 和 .NET 4.5 Framework 中读到了这篇文章(第 19 章,第 746 页)
  • 我刚刚开始阅读您参考的书的部分,绝对一切都是错误的。扔掉它,重新开始。
  • 框架参考是MSDN;浏览网站比获取框架文档的纸质副本要好得多。对于一本关于 C# 的好书,所有标准书都很棒:C# in Depth、Essential C#、C# in a Nutshell。我编辑了所有三个;它们是准确的。
  • 太糟糕了。 “这将导致调用线程休眠”——不,它会导致工作线程休眠。 “我会把这个字符串放到一个新任务中”——不,Run 已经创建的任务会获取这个字符串。写这个东西的人只是在编造东西。这些都不对。就像作者在想“我将如何实现这个功能?”并描述了这一点,而不是描述我们实现它时的实际功能。
  • @BudaGavril:我有一篇 async intro 博客文章可能会对您有所帮助,我在其中尝试介绍如何在不让您不知所措的情况下思考 async/await。

标签: c# async-await thread-state


【解决方案1】:

基本上,关键是程序以同步方式继续执行,直到或除非遇到等待。一旦遇到 await,假设对于一个任务 A,编译器将切换回调用此异步方法的主方法没有 await 关键字,并将继续从任务 A 的调用点作为编译器执行程序知道它必须等待任务 A 的完成,所以为什么不完成它的其他待处理任务。

这里发生的是 button1_Click 事件在 main 方法中没有等待,因此一旦遇到 await DoWork() 它将继续执行。 DoWork() 完成后,编译器将继续执行 button1_Click 中的进一步代码

【讨论】:

  • "button1_Click 事件不在 main 方法中等待" static void Main(string[] args) 入口点吗?
  • 是的。取决于应用程序的类型。基本上从那里调用 button1_Click 时,它不会被等待。
【解决方案2】:

我读过:当逻辑流到达等待令牌时,调用线程将暂停,直到调用完成。

你从哪里读到这些废话?要么有一些你没有引用的上下文,要么你应该停止阅读包含这个的任何文本。 await 的重点是做 相反。等待的目的是在异步任务运行时保持当前线程做有用的工作。

更新:我下载了你引用的书。该部分中的所有内容绝对是错误的。把这本书扔掉,买一本更好的书。

我不明白的是,当我单击按钮时,label1 会有文本 Running 并且标签只有在 10 秒后才会有相同的文本,但是在这 10 秒内我能够输入文本我的文本框,所以似乎主线程正在运行......

没错。以下是发生的事情:

        label1.Text = Thread.CurrentThread.ThreadState.ToString();

文本已设置。

        button1.Text =  await DoWork();

这里发生了很多事情。首先会发生什么? DoWork 被调用。它有什么作用?

        return Task.Run(() => { Thread.Sleep(10000);

它从线程池中抓取一个线程,使该线程休眠十秒钟,然后返回一个代表该线程正在完成的“工作”的任务。

现在我们又回来了:

        button1.Text =  await DoWork();

我们手头有一个任务。 await 首先检查任务是否已经完成。它不是。接下来,它会将此方法的其余部分作为任务的延续。然后它返回给它的调用者。

嘿,它的调用者是什么?我们到底是怎么到这里的?

一些代码调用了这个事件处理程序;它是处理 Windows 消息的事件循环。它看到一个按钮被点击并分派给刚刚返回的点击处理程序。

现在会发生什么?事件循环继续运行。正如您所注意到的,您的 UI 一直运行良好。最终,该线程在 10 秒内结束,任务的继续被激活。那有什么作用?

这会在 Windows 队列中发布一条消息,说“您现在需要运行该事件处理程序的其余部分;我有您要查找的结果。”

主线程事件循环最终会得到该消息。所以事件处理程序从它停止的地方开始:

        button1.Text =  await DoWork();

await 现在从任务中提取结果,将其存储在按钮文本中,然后返回到事件循环。

【讨论】:

  • 我附上了书中的“截图”......也许我弄错了......长话短说:UI线程继续运行,代码的执行在代码中遇到了await关键字,任务完成后继续,对吧?
  • @BudaGavril:这本书在异步等待部分从头到尾都是完全错误的。作者完全误解了。把这本书扔掉,买一本更好的书。
  • @BudaGavril:正确:当await 将控制权返回给调用者时,事件处理程序 的执行将暂停,并在任务完成后的某个时间恢复。 UI 线程 像往常一样继续执行。
【解决方案3】:

通过写await,您是在说- 请在此时停止执行该方法,等待DoWork 完成,然后才能继续。

Asynchronous Programming with async and await (C#) 在 What Happens in an Async Method 部分通过图片逐步解释了 async 方法中发生的事情。

更好的解释是await (C# reference)。看看下面的 WaitSynchronously 和 WaitAsynchronouslyAsync 的 cmets。

文章还指出(强调我的):

await 运算符应用于异步方法中的任务,以暂停方法的执行,直到等待的任务完成。任务代表正在进行的工作。

private async void button1_Click(object sender, EventArgs e)
{
    // Call the method that runs asynchronously.
    string result = await WaitAsynchronouslyAsync();

    // Call the method that runs synchronously.
    //string result = await WaitSynchronously ();

    // Display the result.
    textBox1.Text += result;
}

// The following method runs asynchronously. The UI thread is not
// blocked during the delay. You can move or resize the Form1 window 
// while Task.Delay is running.
public async Task<string> WaitAsynchronouslyAsync()
{
    await Task.Delay(10000);
    return "Finished";
}

// The following method runs synchronously, despite the use of async.
// You cannot move or resize the Form1 window while Thread.Sleep
// is running because the UI thread is blocked.
public async Task<string> WaitSynchronously()
{
    // Add a using directive for System.Threading.
    Thread.Sleep(10000);
    return "Finished";
}

【讨论】:

  • 所以await方法暂停了方法的执行,但是主线程(UI)还在运行?所以“当逻辑流到达等待令牌时,调用线程被挂起,直到调用完成。”只是部分正确?我的意思是不是线程被挂起,而只是方法中的代码执行?
  • @BudaGavril:await 通过 (1) 将方法的其余部分注册为任务的继续,以及 (2) 返回来暂停方法的执行。所有这些工作都是在 UI 线程上完成的。 UI 线程从不 停止运行。该陈述部分不正确;从头到尾都是完全错误的。 await的作用是防止UI线程阻塞,而不是让它阻塞。
【解决方案4】:

async/await 创建一个状态机来为你处理延续。一个非常粗略的等价物(没有一堆特征)是显式延续方法,例如:

private void button1_Click_Continuation(string value)
{
    button1.Text = value;
    label2.Text = Thread.CurrentThread.ThreadState.ToString();
}

private void button1_Click(object sender, EventArgs e)
{
    label1.Text = Thread.CurrentThread.ThreadState.ToString();
    DoWork(button1_Click_Continuation);
}

private void DoWork(Action<string> continuation)
{
    Task.Run(() => {
        Thread.Sleep(10000);
        continuation("done with work");
    });
}

请注意,我仍在使用Task.Run 在单独的线程上运行。请注意,此版本无法跟踪进度(而原版可以将button1_Click的返回值更改为Task以查看是否完成)。

除了上述转换之外,系统还可以判断Task 是否在 UI 线程上启动并再次编组回 UI 线程,以确保一切正常。这可能是您没有意识到的主要事情,但在状态机方面的扩展有时可以解释 asyc/await 的真正含义(而不是让它保持神秘)。

【讨论】:

  • 所以你是说点击事件的代码在另一个线程上运行?因为这可以解释为什么我能够在我的文本框中输入文本。但是从辅助线程更新 UI 怎么样?是否有一些文档说我可以仅通过异步方法(没有其他解决方法)更新 UI 而没有问题,因为您正在从辅助线程更新 UI?
  • @BudaGavril:不,绝对不是。点击事件的代码在 UI 线程上运行;它是一个 UI 事件处理程序! await 将控制权返回给消息循环;这就是为什么您的 UI 不会冻结的原因。您可以不从第二个线程更新 UI;您必须仅从 UI 线程更新 UI。 Await 更容易做到这一点,因为它可以让您更轻松地表达异步工作流停留在 UI 线程上,或者延续 自动封送回。
  • @BudaGavril:这个答案基本正确,但最后一段非常重要。在Task.Run 中对continuation 的调用实际上并没有直接调用延续;它会导致继续在它所属的 UI 线程上运行。请注意,在 console 应用程序中,这是不正确的;可以将延续安排到工作线程上。
猜你喜欢
  • 2019-03-06
  • 1970-01-01
  • 2021-12-12
  • 1970-01-01
  • 2020-07-08
  • 2018-07-09
  • 2013-02-15
  • 1970-01-01
相关资源
最近更新 更多