异步等待:主线程是否会挂起?

10

我正在阅读有关 async/await 关键字的内容,其中提到:

当流程达到 await 标记时,调用线程将被挂起,直到调用完成。

好的,我创建了一个简单的 windows forms 应用程序,放置了两个标签、一个按钮和一个文本框,并编写了以下代码:

        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如何工作呢?

这是书中的“屏幕截图”: enter image description here

敬礼


2
@Noseratio 我在Andrew Troelsen的《Pro C# 5.0和.NET 4.5 Framework》(第19章,第746页)中读到了这个。 - Buda Gavril
6
我刚开始阅读你提到的书籍章节,但是里面的每一个细节都是错误的。请抛弃这本书重新开始吧。 - Eric Lippert
4
参考文献是MSDN框架;浏览网站比获取框架文档的纸质副本更好。关于C#的好书都很棒:《深入理解C#》、《精通C#》、《C#语言精要》。我编辑了这三本,它们非常准确。 - Eric Lippert
4
太可怕了。 "这将导致调用线程睡眠" - 不是,它会导致工作线程睡眠。 "我将把这个字符串放入一个新任务中" - 不是,已经由“Run”创建的任务将获取该字符串。写这个东西的人就像编造东西一样。所有这些都不正确。就像作者想到了“我怎么实现这个功能?”然后描述了那个方法,而不是描述我们实际实现的功能。 - Eric Lippert
3
我有一篇介绍异步编程的博客文章async入门,希望可以帮助你,我试图用简单易懂的方式介绍异步/等待的概念,避免让你感到过于压抑。 - Stephen Cleary
显示剩余10条评论
4个回答

22
我读到过这样的话:当逻辑流程达到"await"标记时,调用线程将被暂停,直到调用完成。
你在哪里读到了那些胡言乱语?要么有一些你没有引用的上下文,要么你应该停止阅读包含这个内容的任何文本。 await 的目的是做相反的事情。 await 的目的是让当前线程在异步任务正在运行时继续执行有用的工作。
更新:我下载了你提到的书。 该部分中绝对所有内容都是错误的。 扔掉这本书,买一本更好的书。
我不明白的是,当我点击按钮时,label1会显示文本“Running”,而只有在10秒钟后标签才会显示相同的文本,但是在这10秒钟内,我能够在我的文本框中输入文本,所以似乎主线程正在运行...
没错。以下是发生的情况:
        label1.Text = Thread.CurrentThread.ThreadState.ToString();

文本已设置。

        button1.Text =  await DoWork();

这里发生了很多事情。最先发生什么?DoWork被调用。它做了什么?

一堆东西在这里发生。最先发生什么?调用DoWork。它做了什么?

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

它从线程池中获取一个线程,使该线程休眠十秒钟,并返回一个表示该线程正在执行的“工作”的任务。

现在我们回到这里:

        button1.Text =  await DoWork();

我们手头上有一个任务。Await 首先检查该任务是否已经完成。它没有完成。然后,它将此方法的其余部分注册为任务的 continuation,然后返回给其调用者。

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

一些代码调用了此事件处理程序;正在处理 Windows 消息的事件循环看到按钮被点击并分派到单击处理程序,它刚刚返回。

现在会发生什么?事件循环继续运行。您的 UI 保持良好运行,正如您所注意到的那样。最终,该线程计时了十秒钟,任务的 continuation 被激活。那会做什么呢?

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

主线程事件循环最终会到达该消息。因此,事件处理程序从中断处继续执行:

        button1.Text =  await DoWork();

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


我已经附上了书中的“屏幕截图”...也许我理解错了...长话短说:当代码中遇到await关键字时,UI线程会继续运行,代码的执行被暂停,并在任务完成后继续进行,对吗? - Buda Gavril
8
从头到尾关于async-await的部分,这本书完全是错误的。作者完全误解了它。把这本书扔掉,找一本更好的书。 - Eric Lippert
@BudaGavril:正确的:当await将控制权返回给调用者时,事件处理程序的执行被暂停,并在任务完成后的某个时间恢复。UI线程的执行仍然像往常一样继续进行。 - Eric Lippert

0
基本上,核心是程序执行在同步方式下继续,直到遇到 await 关键字为止。一旦遇到 await,假设对于任务 A,编译器将返回调用此异步方法的主方法 不使用 await 关键字,并从 Task A 的调用点继续执行程序,因为编译器知道它必须等待完成任务 A,所以为什么不完成其他挂起的任务呢。
在这里,发生的情况是 main 方法中没有等待 button1_Click 事件,因此一旦遇到 await DoWork(),程序将继续执行。在完成 DoWork() 后,编译器将继续执行 button1_Click 中的后续代码。

“button1_Click”事件在主方法中没有被等待。您是指应用程序的static void Main(string[] args)入口点吗? - Theodor Zoulias
是的,这取决于应用程序的类型。基本上,无论从哪里调用button1_Click,都不会等待它完成。 - Rajesh Kumar

0

async/await 创建了一个状态机,为您处理继续操作。一个非常粗略的等价物(没有一堆特性)是显式的 continuation 方法,例如:

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线程,以确保一切都按照您的期望工作。这可能是您没有意识到的主要问题,但有时候对状态机方面进行扩展可以解释async/await的真正含义(而不是让它变得神秘)。

那么你的意思是点击事件中的代码在另一个线程上运行?这可以解释为什么我能够在文本框中输入文本。但是如何从辅助线程更新UI呢?是否有一些文档说明,只要从异步方法(无需其他解决方法)更新UI就没有问题,考虑到你正在从辅助线程更新UI? - Buda Gavril
3
@BudaGavril说:不行,绝对不可以。点击事件的代码在UI线程上运行;它是一个UI事件处理程序!等待操作会将控制权返回到消息循环;这就是为什么您的UI不会冻结的原因。您可能无法从第二个线程更新UI;您必须只能从UI线程更新UI。等待操作使其更容易实现,因为它让您更轻松地表达留在UI线程上的异步工作流程,或者其中的延续会自动地被调度回来 - Eric Lippert
2
@BudaGavril:这个答案基本上是正确的,但最后一段话非常重要。在Task.Run中调用continuation并不会直接调用续体;它会导致续体在UI线程上运行,这是它应该所在的地方。请注意,在控制台应用程序中,情况并非如此;续体可能会被安排到工作线程上。 - Eric Lippert

0
通过编写await,您正在表达 - 请在此处停止方法的执行,等待DoWork完成,然后才继续。
异步方法中发生了什么部分的C#中的异步编程中,有一步一步的解释和图片,说明了async方法中发生了什么。
更好的解释在C#参考文献中。请查看下面WaitSynchronouslyWaitAsynchronouslyAsync的注释。
该文章还指出(重点是我的):

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

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)仍在运行?所以,“当程序流达到 await 标记时,调用线程会被暂停,直到调用完成。”这只是部分正确的吗?我的意思是并不是线程被暂停了,而只是从该方法中执行的代码被暂停了? - Buda Gavril
3
@BudaGavril:await 暂停方法的执行,方法的剩余部分被注册为任务的继续,然后返回。所有这些工作都在 UI 线程上完成。UI 线程 永远不会 停止运行。该语句不是部分正确;从头到尾完全错误。await 的功能是防止UI线程阻塞,而不是导致它阻塞。 - Eric Lippert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接