在WinForms中,为什么不能从其他线程更新UI控件?

18

我相信这背后有一个好的(或至少还算合理的)原因。是什么呢?

7个回答

25
我认为这是一个杰出的问题 - 我认为需要更好的答案。

显然,唯一的原因是在框架中有某些东西不太线程安全。"那个东西"几乎是System.Windows.Forms中每个控件的每个实例成员。

System.Windows.Forms中许多控件的MSDN文档(如果不是所有控件)都说:“此类型的任何公共静态成员(Visual Basic中的Shared)都是线程安全的。不保证任何实例成员是线程安全的。”

这意味着像TextBox.Text {get;set;}这样的实例成员不是可重入的。

使这些实例成员中的每一个都线程安全可能会引入大量开销,而大多数应用程序不需要。相反,.Net框架的设计者决定(我认为是正确的),将多个线程对窗体控件的访问同步的负担放在程序员身上。

[编辑]

虽然这个问题只问了“为什么”,但这里有一篇文章解释了“如何”:

MSDN上的“How to: Make Thread-Safe Calls to Windows Forms Controls”
http://msdn.microsoft.com/en-us/library/ms171728.aspx

1
完全有可能是可重入但不是线程安全的。考虑这个例子 (f) => { x += 1; f(x); x -= 1; },如果f(x)在一个线程上重新进入lambda函数,则直到溢出都没有问题,但如果多线程运行,则会导致x竞争。 - Jeffrey Hantin

9
因为你很容易陷入死锁(以及其他问题)。
例如,你的次要线程可能正在尝试更新UI控件,但UI控件将等待由次要线程锁定的资源被释放,因此两个线程都在等待对方完成。正如其他人所评论的,这种情况并不是UI代码独有的,但却特别普遍。
在其他语言(如C++)中,你可以自由地尝试这样做(没有像WinForms中那样抛出异常),但如果发生死锁,你的应用程序可能会冻结并停止响应。
顺便说一下,你可以轻松地告诉UI线程你想要更新一个控件,只需创建一个委托,然后调用该控件的(异步的)BeginInvoke方法传递你的委托。例如:
myControl.BeginInvoke(myControl.UpdateFunction);

这相当于在工作线程中执行C++/MFC的PostMessage操作。

3
我不确定为什么这个答案被接受了。这根本不是正确的回答。只要有多个线程,死锁就可能发生。GUI程序并没有固有的使它们更容易发生的特点。此外,即使使用BeginInvoke(),也很容易发生死锁。Brian Ensink的答案才是正确的。 - mhenry1384
2
-1:等待锁定资源不是问题。问题在于竞争条件,在任何没有设计用于多线程环境的代码中都存在大量竞争条件。您甚至可以通过将CheckForIllegalCrossThreadCalls设置为false来检查这一点,并观察无死锁操作,直到发生一些有趣的事情。 - Roman Starkov

7
尽管John的答案听起来合理,但实际上,即使使用Invoke,仍然有可能遇到死锁情况。当处理在后台线程上触发的事件时,使用Invoke甚至可能导致这个问题。


真正的原因与竞态条件有关,追溯到古老的Win32时代。我无法在这里解释细节,关键词是消息泵、WM_PAINT事件以及"SEND"和"POST"之间的微妙差别。


更多信息可以在这里这里找到。


2
在1.0/1.1版本中,在调试期间不会抛出异常,相反,你得到的是一种间歇性的运行时挂起情况。很好! :) 因此,在2.0版本中,他们使这种情况抛出异常,这是非常正确的。
实际原因可能是某种并发/锁问题(如Adam Haile所述)。请注意,正常的.NET API(例如TextBox.Text =“Hello”;)包装了需要立即执行的SEND命令,如果在执行更新的线程之外的单独线程上执行,则可能会创建问题。使用Invoke / BeginInvoke使用POST而不是SEND,这将排队操作。
有关SEND和POST的更多信息,请单击此处

1

这样做是为了避免两个事物同时尝试更新控件。(如果CPU在写/读的中间切换到另一个线程,就会发生这种情况) 同样的原因,当访问多个线程之间共享的变量时,需要使用互斥锁(或其他同步方式)。

编辑:

在其他语言(如C++)中,您可以自由尝试并执行此操作(而不像WinForms中抛出异常),但最终会以艰难的方式学习!

啊,是的...我在C/C++和C#之间切换,因此比我应该的更通用,抱歉...他是正确的,你可以在C/C++中这样做,但它会回来咬你一口!


1

还需要在敏感于同时调用的更新函数中实现同步。对于 UI 元素来说,这样做在应用程序和操作系统层面上都是代价高昂的,并且对于绝大多数代码来说完全是多余的。

一些 API 提供了一种改变系统当前线程所有权的方法,因此您可以暂时(或永久)从其他线程更新系统,而无需采用线程间通信。


-1

嗯,我不太确定,但我认为当我们有进度控件,如等待栏、进度条时,我们可以从另一个线程更新它们的值,一切都可以正常工作,没有任何故障。


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