为什么只有UI线程被允许修改UI界面?

25

我知道如果我从不同的线程修改一个控件,需要小心,因为WinForms和WPF不允许从其他线程修改控件的状态。

为什么有这个限制?

如果我能编写线程安全的代码,我应该能够安全地修改控件状态。那么为什么还要存在这个限制呢?


我相信这个限制存在的原因之一是为了始终保证有且仅有一个线程监听Windows消息循环。 - Tim M.
1
@Tim:不对,可以存在多个UI线程,每个线程都有自己的消息循环。例如,可以启动第二个线程来显示进度条对话框。 - Ben Voigt
@Ben:这篇文章标记了C#。在.Net应用程序中,您必须始终将控制权转移到GUI线程以执行与绘图相关的工作(否则会出现不稳定的行为或异常)。当然,这并不妨碍您生成任意数量的线程来执行后台工作。关于监听实际的Win32消息,我所知道的唯一方法(除了外部调用)是使用“protected override void WndProc(ref Message m)”。理论上,可以将传递给此方法的消息并在任何线程上使用它们。 - Tim M.
3
@Tim: 你一直在谈论“GUI线程”。在.NET中可以有多个GUI线程,但GUI组件必须完全分隔。在辅助线程上创建的GUI组件不能直接从主线程使用。在.NET中,当调用 Application.Run 时,线程会运行消息循环,这可以在多个线程上完成。 - Ben Voigt
Application.Run启动一个单独的窗体,在不同的线程上运行其自己的Main()。在单个窗体上,将有一个线程与一个消息队列匹配,用于处理UI事件。控件必须与创建它们的线程匹配。因此,您可以拥有多个窗体,每个窗体都在不同的线程上,并在该线程上处理UI活动。但是,除非在另一个线程上动态创建控件,否则它将需要从指定为UI线程的父窗体的线程中访问。可能有其他方法解决这个问题(我的原始评论以“我相信”开头)。 - Tim M.
@Ben,顺便提一下,我并不反对您的信息,主要是想确保没有人读到这个线程后,会在没有阅读Begin /EndInvoke、Windows消息循环等相关内容的情况下随意启动多个线程。 - Tim M.
6个回答

27

许多GUI框架都有这个限制。根据书籍《Java并发实践》的介绍,这是为了避免复杂的锁定问题。问题在于GUI控件可能需要对UI事件、数据绑定等做出反应,这会导致来自多个不同源头的锁定,从而增加死锁的风险。为了避免这种情况,.NET WinForms(以及其他UI)限制对组件的访问仅限于单个线程,从而避免锁定。


此外,还要考虑与ActiveX控件(如浏览器控件等 - 可能是通过WinForms集成间接实现)的互操作性,这也意味着您拥有一个单线程公寓的COM遗留问题。 - TomTom
3
Windows并没有限制对拥有线程的访问。这种限制是由.NET引入的。 - Ben Voigt
@Ben:我不知道那个。我会更新我的答案来反映那个。我会查一下书上的内容。 - Brian Rasmussen
@Ben:我刚刚查了一下书,它并没有说Windows会这样做,所以错都是我的。无论如何,限制对单个线程的访问确实可以避免锁定,因此我希望答案仍然有用。感谢你的信息 - 我已经使用.NET太久了,所以我只是认为这是Windows的事情。 - Brian Rasmussen
@Ben: 此外,如果我没有记错的话,在调试器中,.NET 甚至直到 2.0 版本才通过 Control.CheckForIllegalCrossThreadCalls 强制实施这一点。 - Michael Petrotta

9
在Windows系统中,当创建控件时,UI更新是通过消息队列中的消息来完成的。程序员无法直接控制消息队列所在的线程,因此控件收到消息后可能会导致其状态改变。如果允许另一个由程序员直接控制的线程更改控件的状态,则必须放置某种同步逻辑以防止控件状态的破坏。.Net中的控件不是线程安全的,我认为这是有意而为之的。在所有控件中放置同步逻辑将在设计、开发、测试和支持提供此功能的代码方面成本昂贵。程序员当然可以为自己的代码提供控件的线程安全性,但不能为与其代码同时运行的.Net代码提供线程安全性。解决此问题的一种方法是将这些类型的操作限制在一个线程上,并仅限于一个线程,这使得.Net中的控件代码更加简单易于维护。

3

.NET保留在您创建控件的线程中随时访问它的权利。因此,来自另一个线程的访问永远不会是线程安全的。


2

Windows支持许多操作,尤其是在组合使用时,本质上不是线程安全的。例如,当一个线程试图将一些文本插入以第50个字符开头的文本字段时,同时另一个线程试图从该字段删除前40个字符,那么应该发生什么?Windows可以使用锁定来确保第二个操作无法开始,直到第一个操作完成,但是使用锁定会增加每个操作的开销,并且还可能导致死锁,如果对一个实体的操作需要操作另一个实体。要求涉及特定窗口的操作必须在特定线程上进行比防止同时执行不安全操作更严格的要求,但相对容易分析。在多个线程中使用控件并通过其他方式避免冲突通常更加困难。


2
你可能能够使自己的代码线程安全,但是你无法向内置的WinForm和WPF代码中注入与你的代码匹配的必要同步原语。请记住,有很多消息在幕后传递,最终导致UI线程访问控件,而你并没有真正意识到这一点。
控件线程亲和性的另一个有趣方面是它可能(尽管我怀疑它们永远不会)使用线程本地存储模式。显然,如果你在创建控件的线程之外的线程上访问控件,无论你如何小心地构造代码来防范所有常见的多线程代码问题,它都无法访问正确的TLS数据。

1

实际上,据我所知,这就是最初的计划!每个控件都可以从任何线程访问!只有当另一个线程需要访问控件时才需要线程锁定,而且由于锁定很昂贵,因此创建了一种新的线程模型称为“线程租赁”。在该模型中,相关控件将被聚合到“上下文”中,仅使用一个线程,从而减少所需的锁定量。相当酷,不是吗?

不幸的是,这种尝试过于大胆,无法成功(并且稍微复杂,因为仍然需要锁定),因此在wPF中再次使用了老式的Windows Forms线程模型--单个UI线程和创建线程来声明对控件的所有权--使我们的生活更加轻松?


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