UI线程有什么特殊之处?

6
假设我有一个同步运行的方法fooCPU(它不调用执行I/O操作的纯异步方法,也不使用其他线程通过调用Task.Run或类似方式来运行其代码)。该方法执行一些繁重的计算 - 它是CPU绑定的。
现在,在我的程序中调用fooCPU而没有将其委托给工作线程来执行。如果fooCPU的某一行需要长时间运行,则在它完成之前不会执行其他行。因此,例如从UI线程调用它会导致UI线程冻结(GUI将变得无响应)。
当我说async/await是多线程的模拟时。两个不同代码段的行交替执行,但只使用单个线程。如果其中一行需要长时间运行,则在它完成之前不会执行其他行。 有人告诉我这对于在UI线程上使用的异步操作是正确的,但对于所有其他情况(ASP.NET、线程池上的异步操作、控制台应用程序等)则不正确。
有人能告诉我这可能意味着什么吗?UI线程与控制台程序的主线程有何不同?
我认为,没有人希望在这个论坛上继续讨论相关话题,例如在评论中出现,因此最好提出一个新问题。

我会在一个Win Forms应用程序和一个控制台应用程序上编译相同的代码,并使用ildasm或reflector检查编译后的IL的差异,以便自己查看。 - Oguz Ozgul
1
@OguzOzgul 这对许多问题都很有用,但不适用于此问题 - 代码完全相同。改变的是全局状态 - 同步上下文的存在。 - Luaan
1
答案很不错,但唯一能满足你好奇心的是详细了解 await 的作用。搜索“.net await internals”看起来很不错。如果你有一个小时的时间,这将回答所有问题,我希望如此。 - usr
看看线程部分中的这篇文章说了什么。我认为这是因为当在UI线程中创建异步任务时,用于回调UI线程的同步上下文总是通过调用this来检索。我很快会确认这一点。由于您的方法始终处于活动状态(没有非阻塞IO操作),因此它通过其执行使用UI线程。 - Oguz Ozgul
似乎UI线程与控制台应用程序的主线程没有什么不同。在控制台主线程上使用await命令等待CPU绑定的异步操作,仍然在同一主线程上运行,并以与阻塞(冻结)UI相同的方式阻止控制台应用程序。我认为这是一个误导性的评论,说控制台应用程序或Asp.Net工作线程上的情况不同。由于您的异步方法实际上没有任何异步操作可执行,因此没有任何可等待的内容,执行永远不会返回到发起线程,无论是Forms还是Console或Web应用程序都无关紧要。 - Oguz Ozgul
3个回答

10

我建议您阅读我的async简介文章,它解释了asyncawait关键字的工作原理。如果您有兴趣编写异步代码,请继续阅读我的async最佳实践文章

简介文章的相关部分:

异步方法的开始执行方式与任何其他方法相同。也就是说,它会同步运行,直到遇到“await”(或抛出异常)。

这就是为什么您控制台代码示例中的内部方法(没有await)同步运行的原因。

等待检查该可等待项是否已经完成;如果可等待项已经完成,则该方法只是继续运行(同步运行,就像常规方法一样)。

这就是为什么您控制台代码示例中等待内部方法的外部方法(该内部方法是同步的)同步运行的原因。

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

这个“上下文”是SynchronizationContext.Current,除非它为null,否则它是TaskScheduler.Current。或者,更简单的版本:

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

  1. 如果您在UI线程上,则它是UI上下文。
  2. 如果您正在响应ASP.NET请求,则它是ASP.NET请求上下文。
  3. 否则,通常是线程池上下文。
  4. 将所有内容综合起来,您可以将async/await可视化为以下方式:该方法分成几个“块”,每个await充当方法被拆分的点。第一个块总是同步运行,在每个拆分点,它可以继续同步或异步运行。如果它以异步方式继续,则默认情况下将在捕获的上下文中继续。UI线程提供将在UI线程上执行下一个块的上下文。
    因此,回答这个问题,UI线程的特殊之处在于它们提供了一个SynchronizationContext,用于将工作项排队返回到相同的UI线程。

    很显然,Stack Overflow旨在成为问答网站,而不是论坛。因此,它不适合请求详尽的教程;它是一个困难时寻求帮助的地方,或者当您已经尽最大努力进行研究但仍无法理解某些内容时。这就是为什么SO上的评论受到限制的原因-它们必须简短,没有良好的代码格式等。此网站上的评论旨在澄清问题,而不是作为讨论或论坛主题。

谢谢。我已经阅读了一些 MSDN 博客和你的博客文章了。“UI 线程提供了一个上下文,将在 UI 线程上执行下一个块” - 这并不是从你的介绍帖子关于 async/await 的内容中得出的结论,对吧?我认为上下文不是线程标识符(多个线程可以共享一个上下文)。关于“到底什么是‘上下文’?简单的答案:如果你在 UI 线程上,那么它就是一个 UI 上下文。”- 它并没有解释 context 到底是什么。 - user4205580
所以我猜测,“UI线程提供了一个上下文,将在UI线程上执行下一个块”源自于这里的引用:“当前实现为每个UI线程创建一个WindowsFormsSynchronizationContext”。 - user4205580
@user4205580:在简介文章中有提到,在“简单”解释之后的下一段中:如果SynchronizationContext.Current不为null,则它是当前的SynchronizationContext。(UI和ASP.NET请求上下文是SynchronizationContext上下文)。 - Stephen Cleary
是的,但它并没有说明上下文的行为方式,就像你在文章中所解释的那样。我们不知道上下文是否会在UI线程上执行异步方法的继续,直到我们了解使用了什么SynchronizationContext。别误会,我不是在抱怨,我只想知道我是否应该自己弄清楚这个问题。 - user4205580
我所指的只是这句话:“UI线程提供了一个上下文,该上下文将在UI线程上执行下一个块”,并不是从你的博客文章中得出的结论。我们知道下一个块将在UI上下文中执行,但是如果在UI上下文(当前同步上下文)上执行意味着在UI线程上执行,我们就不知道了。我们需要了解UI线程同步上下文的详细信息: “当前实现为每个UI线程创建一个WindowsFormsSynchronizationContext。”,它告诉了我们这一点。 - user4205580

9
很简单,一个线程一次只能做一件事情。如果你让UI线程去做与UI无关的事情,比如数据库查询,那么所有UI活动都会停止。不再有屏幕更新,也没有鼠标点击和键盘按键的响应。它看起来和行为像是“冻结”。
你可能会说,“好吧,我就用另一个线程来处理UI。”在控制台模式下可以工作,但在GUI应用程序中,使代码线程安全很困难,因为涉及到了大量的代码。而且UI根本不是线程安全的,因为使用了复杂的类库封装。
通用解决方案是将非UI相关的工作放在工作线程上完成,让UI线程仅负责处理简单的UI操作。异步/等待帮助您实现这一点,await右侧的内容在工作线程上运行。唯一会出错的方式(这种情况并不少见)就是要求UI线程仍然做太多的工作,比如每毫秒向文本框添加一行文本。这只是糟糕的UI设计,人类不会阅读得那么快。

“await 右侧的内容运行在一个 worker 上”- 我认为我已经读了很多答案(包括在 SO 上),都说 await 本身不会创建或切换线程。通过 await syncFoo() 调用同步方法并不会使它在不同的线程上执行。实际上,syncFoo()await syncFoo() 之间没有任何区别。 - user4205580
当然,它不会。但是任何理智的人都不会将任何非异步操作放在右侧。而且 await 绝对会帮助任何人获得正确的思路,不是所有东西都适合放在那里。 - Hans Passant
抱歉,也许我错了,但我认为即使该方法是异步的,async本身也不会使其在工作线程上执行 - 这取决于该方法是否调用Task.Run。纯异步方法通常不使用其他线程,它们在比线程更低的级别上执行I/O操作(http://blog.stephencleary.com/2013/11/there-is-no-thread.html)。 - user4205580
重要的不是等待代码在工作线程上运行,因为让代码在工作线程上运行非常容易。真正重要的是接下来会发生什么。让代码在工作线程完成后运行并在正确的线程上运行。还要处理工作线程代码失败的情况,并知道如何在需要时停止它。这些都比较困难。 - Hans Passant
如果该方法是异步的,**await** 本身不会使其在工作线程上执行 - 我指的是那里的 await。因此,“在 await 右侧的内容在工作线程上运行”这个说法并不完全正确,因为它完全取决于 await 右侧的内容在内部执行了什么操作。无论 foo 是同步还是异步,await foo() 都不会使 foo 在单独的线程上运行。 - user4205580

3

鉴于

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

你说得没错,如果在UI线程上调用Foo(),那么Bar()会立即被调用,Baz()稍后仍然在UI线程上被调用。

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

实际上正在发生的是将此方法拆分为类似于以下内容的内容:

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

这并不完全正确,但错误的方式并不重要。

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

实际上,任务本身并不决定,它们只是将委托传递给SynchronizationContext。正是这个同步上下文决定了要执行的代码该做什么。

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

ASP.NET、后台线程,它们都有不同的同步上下文,这就导致了代码调度方式的变化。


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