GUI线程安全详解(三)
继第一和第二篇后,GUI线程安全的原理性内容基本就这些了,如果你是搞学术理论研究的基本就不用继续阅读下面几篇我要继续八卦的内容,下面的内容都是针对具体技术平台的细节问题了。
static void Main(string[] args) { TextBlock text = new TextBlock(); }
创建个Console程序,敲出上面的和helloworld一样复杂度的代码,运行后你估计会怀疑自己的智商,这么简单的代码都能搞出异常?
还真是再简单问题都有其复杂的一面,我总在想为什么傻人有傻福,因为傻人傻傻的认真对待每个简单的事情所以搞透了其复杂的本质,所谓的聪明人都去研究哥德巴赫猜想,日积月累傻人掌握了众多事物本质成了有福之人,所谓聪明者竟然没搞明白一加一为什么等于二。
private InputManager() { // STA Requirement // // Avalon doesn't necessarily require STA, but many components do. Examples // include Cicero, OLE, COM, etc. So we throw an exception here if the // thread is not STA. if(Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) { throw new InvalidOperationException(SR.Get(SRID.RequiresSTA)); }
看看System.Windows.Input.InputManager的代码实现你就明白了,原来TextBlock的创建要求在STA环境中,你要问我啥叫STA,我喜欢将其简单理解为这玩意儿就是我们前面提到的EDT,也就是那个全局唯一的UI Thread,具体的解释你可以看Component_Object_Model#Threading_in_COM的解释,当年读潘爱民翻译的《COM本质论》差点让我没了继续当程序员的勇气,以下引用了一段话你就看着玩就行
There are three types of Apartment Models in the COM world: Single-Threaded Apartment (STA), Multi-Threaded Apartment (MTA), and Neutral Apartment. Each apartment represents one mechanism whereby an object’s internal state may be synchronized across multiple threads.
The Single-Threaded Apartment (STA) model is a very commonly used model. Here, a COM object stands in a position similar to a desktop application’s user interface. In an STA model, a single thread is dedicated to drive an object’s methods, i.e. a single thread is always used to execute the methods of the object. In such an arrangement, method calls from threads outside of the apartment are marshalled and automatically queued by the system (via a standard Windows message queue). Thus, there is no worry about race conditions or lack of synchronicity because each method call of an object is always executed to completion before another is invoked.
上面的一行代码其实已经说明了问题Thread.CurrentThread.GetApartmentState() != ApartmentState.STA,也就是main启动运行的线程并不是EDT,回想一下WindowsForms和WPF的应用程序的main是怎么写的,是的就是这个[STAThread]起的作用,把上面代码加上这个[STAThread]你就不会再怀疑自己智商了。
[STAThread]
static void Main()
{
... ...
}
《Applications = Code + Markup》这本是我学习WPF的第一本书,讲解思路非常符合传统程序员口味,上半本先完全用OO的方式解释了WPF组件的来龙去脉,下半本才开始设计XAML的标签应用,说实话我很看不惯现在的快餐式书籍,一上来就是XAML用户连WPF组件Dependency Properties这种最重要的新机制都完全不明白的情况下就开始写项目,同样的还有一上来就开始鼓吹如何用MXML快速创建Flex程序,本末倒置不介绍背后ActionScript语言实现原理的书籍,更有一大堆历史杯具人物写了多年JSP不知道啥是Java的OO。
前几天逛书店发现蔡学庸也翻译了《Applications = Code + Markup》一书,刚毕业时买过他的一本经典《Java夜未眠》,那时候作为个junior程序员的我手头也几个钱,晚上也没法去新天地泡mm的方式夜未眠,只好翻翻学庸兄的《Java夜未眠》,不过这些年薪水提高了反而不去书店买书了,原因很简单译书上得不到最新的技术资料而且翻译往往后失去作者的原味,相信《红楼梦》的译书很难接近原著的味道,扯远了,我不是要推销《Applications = Code + Markup》,更不是要打击蔡学庸译书的销量(我还是很尊重且喜欢订阅学庸兄的blog,如果学庸看到小弟此文希望能有机会交个朋友),只是想引用书中一段话帮助大家理解:
In any WPF program, the [STAThread] attribute must precede Main or the C# compiler will complain. This attribute directs the threading model of the initial application thread to be a single-threaded apartment, which is required for interoperability with the Component Object Model (COM). “Single-threaded apartment” is an old COM-era, pre-.NET programming term, but for our purposes you could imagine it to mean our application won’t be using multiple threads originating from the runtime environment.
使用TWaver Java的客户应该有印象,TWaver Java Demo的启动代码和地球上99%的Swing的main代码不一样,套了个SwingUtilities.invokeLater,曾经有个客户程序启动再大部分机器上都正常,结果在一台双核(N年前双核可是很洋相配置)的机器启动总出错,去现场帮忙找原因时我查的第一行代码就是main的启动代码,发现用户直接就在main里面创建各种UI组件,同时还起了Thread做各种业务,我就让他改成和TWaver Demo一样,启动时套了个SwingUtilities.invokeLater,你猜怎么样我竟然“被”允许回家了,这是我在赛瓦这些年最短的现场支持经历,像我这种不敢坐飞机的(去年做了38个小时火车去了哈尔滨培训客户)做了十几个小时的火车竟然现场待了几分钟就回家了,实在是亏啊。
public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable(){ public void run() { Demo.init(); DemoFrame demoFrame = new DemoFrame(); demoFrame.setVisible(true); } }); }
很简单,因为main启动的函数并不是Swing的事件派发线程,SwingUtilities.isEventDispatchThread()你可以调用该函数测试一下,而地球99%的代码直接非EDT里面做UI的工作时极其危险的,大部分程序都是初始化界面然后show就结束了,这样基本能侥幸不出错,但如果你调用了show之后再有代码在执行,而且是操作到UI组件的地方拿基本必然会导致问题,但为了您的生命安全还请遵守GUI线程安全规则。这里有更多的解释可以参考:
public class MyApplication { public static void main(String[] args) { JFrame f = new JFrame(“Labels”); // Add components to // the frame here… f.pack(); f.show(); // Don’t do any more GUI work here… } } All the code shown above runs on the “main” thread. The f.pack() call realizes the components under the JFrame. This means that, technically, the f.show() call is unsafe and should be executed in the event-dispatching thread. However, as long as the program doesn’t already have a visible GUI, it’s exceedingly unlikely that the JFrame or its contents will receive a paint() call before f.show() returns. Because there’s no GUI code after the f.show() call, all GUI work moves from the main thread to the event-dispatching thread, and the preceding code is, in practice, thread-safe.
另外我挺喜欢现在.NET的线程安全问题上再底层框架做的检查工作,就像刚才Console操作TextBlock的代码,很容易程序员就能知道问题所在,而Swing这点上是最让我不满意的地方,你不提示检查我这些在普通线程操作UI组件就算了,某些情况下Swing还自作聪明的在内部帮你SwingUtilities.invoke***,结果常常导致很多客户质问我为什么我在普通线程调用revalidate()不出错,你们还非得让我以这么丑陋的代码方式到处去SwingUtilities.invoke,我觉得Swing这样的实现会误导程序员放松对线程安全的戒备,Swing仅仅在很少很少很少的函数上做了判断内部处理,如果大家觉得JComponent.revalidate()可以随意调用,那岂不TableModel、TreeModel…包括TWaver的DataBox和Element都可以不管EDT随意调用了,所以这点上.NET严谨的判断还是能让程序员尽早的规避了很多线程安全问题
public void revalidate() { if (getParent() == null) { // Note: We don't bother invalidating here as once added // to a valid parent invalidate will be invoked (addImpl // invokes addNotify which will invoke invalidate on the // new Component). Also, if we do add a check to isValid // here it can potentially be called before the constructor // which was causing some people grief. return; } if (SwingUtilities.isEventDispatchThread()) { invalidate(); RepaintManager.currentManager(this).addInvalidComponent(this); } else { Runnable callRevalidate = new Runnable() { public void run() { revalidate(); } }; SwingUtilities.invokeLater(callRevalidate); } }
今天要讲的就这些内容,不知道昨天提到TWaver的FileTreeDemo里面还有个多线程的细节,你有没有注意到tree在加载文件是还能看到gif的loading图标,你只要打开tree.setEnableAnimation(true)开关,随时随意间任何element.setIcon(“/demo/tree/file/loading.gif”)设置上任何gif图标,twaver将透明自动的识别gif动画图标,并且让tree上的icon动画起来。这还没啥,有一回我看到用户启动画面竟然有很酷动画的splash screen,我问用户是不是嵌入了Flash,用户说:“用的就是你们的Network做启动界面,只不过放了一个GIF的大Node做主启动界面而已,赛瓦真该开除了你这个傻B,还号称TWaver Evangelist,丢人也不能丢这么大啊”