【问题标题】:What's so special about UI thread?UI线程有什么特别之处?
【发布时间】:2015-11-16 08:31:15
【问题描述】:

假设我有一个同步运行的方法 fooCPU(它不调用执行 I/O 的纯异步方法,或使用其他线程通过调用 Task.Run 或类似方式运行其代码)。该方法执行一些繁重的计算 - 它受 CPU 限制。

现在我在我的程序中调用fooCPU 而不委托它由工作线程执行。如果fooCPU 的一行需要很长时间才能运行,那么在它完成之前不会执行其他行。例如,从 UI 线程调用它会导致 UI 线程冻结(GUI 将变得无响应)。

当我说 async/await 是对多线程的模仿时。两个不同的代码行在一个线程上轮流执行。如果其中一行需要很长时间才能运行,则在完成之前不会执行其他行。,

有人告诉我,在 UI 线程上使用异步是正确的,但对于所有其他情况(ASP.NET、线程池上的异步、控制台应用程序等)并非如此。

谁能告诉我这可能意味着什么? UI 线程与控制台程序的主线程有何不同?

我认为没有人希望这个论坛上的任何人继续讨论相关主题,例如它们出现在 cmets 中,所以最好提出一个新问题。

【问题讨论】:

  • 我将在 win forms 应用程序和控制台应用程序上编译相同的代码,并使用 ildasm 或反射器检查编译后的 IL 的差异,以亲自查看。
  • @OguzOzgul 这对很多问题都很有用,但不是这个——代码完全相同。改变的是全局状态 - 同步上下文的存在。
  • 答案非常好,但唯一能满足你好奇心的就是详细了解 await 的作用。搜索“.net await internals”看起来不错。如果你有一个小时的时间,我希望这能回答所有问题。
  • 看看this in the threads section says 怎么说。我认为这是因为当在 UI 线程中创建异步任务时,用于回调 UI 线程的同步上下文总是通过调用this 来检索。我会尽快确认这一点。由于您的方法始终处于活动状态(没有非阻塞 io 操作),因此它在执行过程中使用 UI 线程。
  • 看起来,UI 线程与控制台应用程序的主线程没有什么不同。一个 cpu 绑定的异步操作,在控制台主线程上使用 await 命令等待,仍然在同一个主线程上运行,并阻止控制台应用程序,就像它阻止(冻结)UI 一样。我认为这是一个误导性的评论,说在控制台应用程序或 Asp.Net 工作线程上的情况有所不同。由于您的 async 方法实际上没有执行任何异步操作,因此没有什么可等待的,并且执行永远不会返回到启动线程,与 Forms、Console 或 Web 应用程序无关

标签: c# multithreading async-await


【解决方案1】:

我建议你阅读我的async intro post;它解释了asyncawait 关键字的工作原理。然后,如果您对编写异步代码感兴趣,请继续使用我的async best practices article

介绍帖的相关部分:

异步方法的开头与任何其他方法一样执行。也就是说,它会同步运行,直到遇到“等待”(或引发异常)。

这就是inner method in your console code example(没有await)同步运行的原因。

Await 检查 awaitable 是否已经完成;如果 awaitable 已经完成,则该方法继续运行(同步地,就像常规方法一样)。

这就是为什么outer method in your console code example(即awaiting 同步的内部方法)同步运行的原因。

稍后,当 awaitable 完成时,它将执行 async 方法的其余部分。如果您正在等待内置的可等待对象(例如任务),则 async 方法的其余部分将在“等待”返回之前捕获的“上下文”上执行。

这个“上下文”是SynchronizationContext.Current,除非它是null,在这种情况下它是TaskScheduler.Current。或者,更简单的版本:

“上下文”到底是什么? 简单的回答:

  1. 如果您在一个 UI 线程上,那么它就是一个 UI 上下文。
  2. 如果您正在响应 ASP.NET 请求,那么它就是一个 ASP.NET 请求上下文。
  3. 否则,通常是线程池上下文。

将所有这些放在一起,您可以将async/await 想象成这样工作:该方法被分成几个“块”,每个await 充当该方法被拆分的点。第一个块始终同步运行,并且在每个拆分点它可以同步或异步继续。如果它异步继续,那么它将在捕获的上下文中继续(默认情况下)。 UI 线程提供了一个上下文,它将在 UI 线程上执行下一个块。

所以,要回答这个问题,UI 线程的特殊之处在于它们提供了一个SynchronizationContext,队列可以返回到同一个 UI 线程。

我认为没有人希望这个论坛上的任何人继续讨论相关主题,例如它们出现在 cmets 中,所以最好提出一个新问题。

嗯,Stack Overflow 专门打算成为一个论坛;这是一个问答网站。所以这不是一个要求详尽教程的地方;当您被困在试图让代码正常工作时,或者如果您在研究了所有有关它的所有内容后不理解某些东西时,这是一个可以来的地方。这就是为什么 SO 上的 cmets (故意)受到限制的原因 - 它们必须简短,没有漂亮的代码格式等。本网站上的评论旨在澄清,而不是作为讨论或论坛主题。

【讨论】:

  • 谢谢。实际上,我已经阅读了一些 msdn 博客和您的博客文章。 'UI 线程提供了一个上下文,它将在 UI 线程上执行下一个块' - 它并没有遵循您的介绍性帖子中关于异步/等待的内容,是吗?我认为上下文不是线程的标识符(许多线程可以共享一个上下文)。关于'那个“上下文”到底是什么?简单的回答:如果你在一个 UI 线程上,那么它就是一个 UI 上下文。' - 它没有解释 context 到底是什么。
  • 所以我猜'UI线程提供了一个上下文,它将在UI线程上执行下一个块'来自this,引用:当前实现为每个UI线程创建一个WindowsFormsSynchronizationContext.
  • @user4205580:在介绍文章中,在“简单”解释之后的下一段中:If SynchronizationContext.Current is not null, then it’s the current SynchronizationContext. (UI and ASP.NET request contexts are SynchronizationContext contexts).
  • 是的,但它没有说明上下文的行为方式,正如您在 article 中解释的那样。我们不知道上下文是否会在 UI 线程上执行异步方法的 contunation,直到我们了解了使用 SynchronizationContext 的内容。不要误会我的意思,我不是在抱怨,我只是想知道我是否应该自己弄清楚。
  • 我的意思是这句话:UI 线程提供了一个上下文,将在 UI 线程上执行下一个块 并没有从您的博客文章中得到。我们知道下一个块将在 UI 上下文上执行,但是如果在 UI 上下文(当前同步上下文)上执行意味着在 UI 线程上执行,我们不知道。我们需要关于 UI 线程 SynchronizationContext 做什么的详细信息:当前实现为每个 UI 线程创建一个 WindowsFormsSynchronizationContext。,它告诉我们。
【解决方案2】:

这很简单,一个线程一次只能做一件事。因此,如果您将 UI 线程发送到树林中执行与 UI 无关的操作,例如 dbase 查询,那么所有 UI 活动都会停止。没有更多的屏幕更新,没有对鼠标点击和按键的响应。它看起来和行为冻结

你可能会说,“好吧,那我就用另一个线程来做 UI”。在控制台模式下工作,有点。但不是在 GUI 应用程序中,使代码线程安全是困难的,而且 UI 根本不是线程安全的,因为涉及到太多代码。不是你写的那种,是你用花哨的类库包装器使用的那种。

通用的解决方案是将其反转,在工作线程上执行与 UI 无关的工作,让 UI 线程只处理简单的 UI 工作。 Async/await 可以帮助您做到这一点,await 右侧的内容在工作人员上运行。唯一能解决这个问题的方法是让 UI 线程仍然做太多的工作,这种情况并不少见。就像每毫秒向文本框添加一行文本一样。这只是糟糕的 UI 设计,人类的阅读速度不会那么快。

【讨论】:

  • 'await 右侧的内容在工人身上运行' - 我想我已经阅读了很多答案(这里也是如此),指出 await 本身不会创建或切换线程。通过await syncFoo() 调用同步方法不会使其在不同的线程上执行。其实syncFoo()await syncFoo()是没有区别的。
  • 当然,它没有。但是没有人会在他的头脑中将任何非异步的东西放在右边。并且 await 肯定会帮助任何人获得正确的想法,而不是一切都在那里。
  • 对不起,也许我错了,但我认为即使该方法是异步的,异步本身也不会使其在工作线程上执行 - 这取决于该方法是否调用 @987654326 @ 例如。纯异步方法通常不使用其他线程,它们在 lower level than threads 上执行 I/O 操作。
  • 等待代码在工作线程上运行一点也不重要。让代码在 worker 上运行非常非常容易。 真正重要的是接下来会发生什么。在工作人员完成后让代码运行并让它在正确的线程上运行。并处理工人代码失败。并且知道如何让它在你想停下来的时候停下来。困难得多。
  • '如果方法是异步的,await 本身不会让它在工作线程上执行' - 我的意思是在那里等待。所以“await右边的东西在worker上运行”的说法并不完全正确,因为这完全取决于await右边的那个东西在里面做了什么。 await foo() 不会使 foo 在单独的线程上运行,无论 foo 是否同步。
【解决方案3】:

给定

async void Foo() {
   Bar();
   await Task.Yield();
   Baz();
}

你说得对,如果 Foo() 在 UI 线程上被调用,那么 Bar() 会立即被调用,Baz() 会在稍后的某个时间被调用,但仍在 UI 线程上。

但是,这不是线程本身的属性。

实际发生的情况是这个方法被拆分成类似于

Task Foo() {
   Bar();
   return Task.Yield().Continue(() => {
     Baz();
   });
}

这实际上并不正确,但错误的方式并不重要。

传递给我假设的Continue 方法的参数是可以以某种方式调用以由任务确定的代码。任务可能决定立即执行它,它可能决定稍后在同一线程上执行它,或者它可能决定稍后在不同线程上执行它。

实际上,任务本身并不决定,它们只是将委托传递给SynchronizationContext。正是这个同步上下文决定了如何处理待执行的代码。

这就是线程类型之间的区别:一旦您从线程访问 any WinForms 控件,WinForms 就会为该特定线程安装一个同步上下文,这将安排要执行的代码稍后在同一线程上。

ASP.NET、后台线程,都是不同的同步上下文,这就是导致代码调度方式发生变化的原因。

【讨论】:

    猜你喜欢
    • 2011-04-18
    • 2017-02-19
    • 2016-09-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-23
    • 2021-01-01
    相关资源
    最近更新 更多