为什么GUI是单线程的

早期的GUI应用程序都是单线程的,并且GUI事件在“主事件循环”进行处理。当前的GUI框架则使用了一种略有不同的模型:在该模型中创建一个专门事件分发线程(Event Dispatch Thread,EDT)来处理GUI 事件.

单线程的GUI 框架并不仅限于在Java中,在Qt、NexiStep、MacOS Cocoa、X Windows 以及其他环境中的GUI框架都是单线程的。许多人曾经尝试过编写多线程的GUI框架,但最终都由于竞态条件和死锁导致的稳定性问题而又重新回到单线程的事件队列模型:采用一个专门的线程从队列中抽取事件,并将它们转发到应用程序定义的事件处理器。(AWT最初尝试在更大程度上支持多线程访问,而正是基于在AWT中得到的经验和教训, Swing 在实现时决定采用单线程模型。)

在多线程的GUI框架中更容易发生死锁问题,其部分原因在于,在输入事件的处理过程与GUI组件的面向对象模型之间会存在错误的交互。用户引发的动作将通过一种类似于“气泡上升”的方式从操作系统传递给应用程序——操作系统首先检测到一次鼠标点击,然后通过工具包将其转化为“鼠标点击”事件,该事件最终被转换为一个更高层事件(例如“鼠标键被按下”事件)转发给应用程序的监听器。另一方面,应用程序引发的动作又会以“气泡下沉”的方式从应用程序返回到操作系统。例如,在应用程序中引发修改某个组件背景色的请求,该请求将被转发给某个特定的组件类,并最终转发给操作系统进行绘制。因此,一方面这组操作将以完全相反的顺序来访问相同的GUI对象;另一方面又要确保每个对象都是线程安全的,从而导致不一致的锁定顺序,并引发死锁(请参见第10章)。这种问题几乎在每次开发GUI工具包时都会重现。

另一个在多线程GUI框架中导致死锁的原因就是“模型一视图一控制(MVC)”这种设计模式的广泛使用。通过将用户的交互分解到模型、视图和控制等模块中,能极大地简化GUI应用程序的实现,但这却进一步增加了出现不一致锁定顺序的风险。“控制”模块将调用“模型”模块,而“模型”模块将发生的变化通知给“视图”模块。“控制”模块同样可以调用“视图”模块,并调用“模型”模块来查询模型的状态。这将再次导致不一致的锁定顺序并出现死锁。

Sun 公司的前副总裁Graham Hamilton 在其博客中总结了这些问题,详细阐述了为什么多线程的GUI 工具包会成为计算机科学史上的又一个“失败的梦想”。

不过,我相信你还是可以成功地编写出多线程的GUI工具包,只要做到:非常谨慎地设计多线程GUI工具包,详尽无遗地公开工具包的锁定方法,以及你非常聪明,非常仔细,并且对工具包的整体结构有着全局理解。然而,如果在上述某个方面稍有偏差,那么即使程序在大多数时候都能正确运行,但在偶尔情况下仍会出现(死锁引起的)挂起或者(竞争引起的)运行故障。只有那些深入参与工具包设计的人们才能够正确地使用这种多线程的GUI 框架。

然而,我并不认为这些特性能够在商业产品中得到广泛使用。可能出现的情况是:大多数普通的程序员发现应用程序无法可靠地运行,而又找不出其中的原因。于是,这些程序员将感到非常不满,并诅咒这些无辜的工具包。

单线程的GUI框架通过线程封闭机制来实现线程安全性。所有GUI对象,包括可视化组件和数据模型等,都只能在事件线程中访问。当然,这只是将确保线程安全性的一部分工作交给应用程序的开发人员来负责,他们必须确保这些对象被正确地封闭在事件线程中。

  串行事件处理

GUI应用程序需要处理一些细粒度的事件,例如点击鼠标、按下键盘或定时器超时等。事件是另一种类型的任务,而AWT和Swing提供的事件处理机制在结构上也类似于Executor。

因为只有单个线程来处理所有的GUI任务,因此会采用依次处理的方式——处理完一个任务后再开始处理下一个任务,在两个任务的处理过程之间不会重叠。清楚了这一点,就可以更容易地编写任务代码,而无须担心其他任务会产生干扰。

串行任务处理不利之处在于,如果某个任务的执行时间很长,那么其他任务必须等到该任务执行结束。如果这些任务的工作是响应用户输入或者提供可视化的界面反馈,那么应用程序看似会失去响应。如果在事件线程中执行时间较长的任务,那么用户甚至无法点击“取消”按钮,因为在该这个任务完成之前,将无法调用“取消”按钮的监听器。因此,在事件线程中执行的任务必须尽快地把控制权交还给事件线程。要启动一些执行时间较长的任务,例如对某个大型文档执行拼写检查,在文件系统中执行搜索,或者通过网络获取资源等,必须在另一个线

                  

http://weblogs.java.net/blog/kgh/archive/2004/10。

程中执行这些任务,从而尽快地将控制权交还给事件线程。如果要在执行某个时间较长的任务时更新进度标识,或者在任务完成后提供一个可视化的反馈,那么需要再次执行事件线程中的代码。这也很快会使程序变得更复杂。

   Swing中的线程封闭机制

所有Swing组件(例如JButton和JTable)和数据模型对象(   例如TableModel和TreeModel)都被封闭在事件线程中,因此任何访问它们的代码都必须在事件线程中运行。GUI 对象并非通过同步来确保一致性,而是通过线程封闭机制。这种方法的好处在于,当访问表现对象(Presentation Object)时在事件线程中运行的任务无须担心同步问题,而坏处在于,无法从事件线程之外的线程中访问表现对象。

Swing 的单线程规则是:Swing中的组件以及模型只能在这个事件分发线程中进行创建、修改以及查询。

与所有的规则相同,这个规则也存在一些例外情况。Swing 中只有少数方法可以安全地从其他线程中调用,而在Javadoc中已经很清楚地说明了这些方法的线程安全性。单线程规则的其他一些例外情况包括:

·SwingUtilities. isEventDispatchThread,用于判断当前线程是否是事件线程。

·SwingUtilities. invokeLater,该方法可以将一个Runnable 任务调度到事件线程中执行(可以从任意线程中调用)。

·SwingUtilities. invokeAndWait,该方法可以将一个Runnable 任务调度到事件线程中执行,并阻塞当前线程直到任务完成(只能从非GUI线程中调用)。

·所有将重绘(Repaint)请求或重生效(Revalidation.)请求插入队列的方法(可从任意线程中调用)。

·所有添加或移除监听器的方法(这些方法可以从任意线程中调用,但监听器本身一定要在事件线程中调用)。

invokeLater 和invokeAndWait 两个方法的作用酷似Executor。事实上,用单线程的Executor 来实现SwingUtilities中与线程相关的方法是很容易的,如程序清单9-1 所示。这并非SwingUtilities 的真实实现,因为Swing的出现时间要早于Executor 框架,但如果现在来实现Swing,或许应该采用这种实现方式。

                              程序清单9-1使用 Executor 来实现 Swing Utilities                     

public class SwingUtilities {

private static final ExecutorService exec =

Executors,newSingleThreadExecutor(new SwingThreadFactory());

private static volatile Thread swingThread;

private static class SwingThreadFactory implements ThreadFactory {public Thread newThread (Runnable r){

swingThread =new Thread(r);

return swingThread;

}

}

public static boolean isEventDispatchThread(){

return Thread. currentThread()==swing Thread;

}

public static void invokeLater(Runnable task){

exec. execute(task);

}

public static void invokeAndWait(Runnable task)

throws InterruptedException,InvocationTargetException {

Future f =exec. submit(task);

try {

f. get ( ) ;

}catch (ExecutionException e){

throw new InvocationTargetException(e);

}

}

}

                                                                      

可以将Swing 的事件线程视为一个单线程的Executor,它处理来自事件队列的任务。与线程池一样,有时候工作者线程会死亡并由另一个新线程来替代,但这一切要对任务透明。如果所有任务的执行时间都很短,或者任务调度的可预见性并不重要,又或者任务不能被并发执行,那么应该采用串行的和单线程的执行策略。

程序清单9-2 中的GuiExecutor是一个Executor,它将任务委托给SwingUtilities来执行。也可以采用其他的GUI框架来实现它,例如SWT 提供的Display. asyncExec方法,它类似于Swing 中的invokeLater。

                  程序清单9-2      基于SwingUtilities构建的 Executor                     

public class GuiExecutor extends AbstractExecutorService {

//采用   “单件(Singleton)”模式,有一个私有构造函数和一个公有的工厂方法

private static final GuiExecutor instance =new GuiExecutor();

private GuiExecutor(){}

public static GuiExecutor instance(){return instance;}

public void execute (Runnable r){

if (SwingUtilities. isEventDispatchThread())

r. run ( ) ;

else

SwingUtilities. invokeLater(r);

//其他生命周期方法的实现

}

猜你喜欢

转载自blog.csdn.net/2301_78064339/article/details/131031302