客户机的线程问题

当使用小窗口工具箱时,了解用于阅读和调度平台 GUI 事件的基础线程模型是很重要的。当在应用程序的代码中使用 Java 线程时,UI 线程的实现将影响应用程序必须遵循的规则。

本机事件调度

在任何 GUI 应用程序下面,不管语言还是 UI 工具箱,OS 平台正在检测 GUI 事件, 并将它们放在应用程序事件队列中。尽管在不同的 OS 平台上机制稍微有些不同,但是基础是相似的。当用户单击鼠标、输入字符或者对窗口的外观进行处理时,OS 将生成应用程序 GUI 事件,例如,鼠标单击、击键或者窗口绘制事件。它确定哪个窗口(和应用程序)应该接收事件并将它放在应用程序的事件队列中。

任何窗口化的 GUI 应用程序的基础结构都是事件循环。应用程序进行初始化,然后启动循环,它只从队列中阅读 GUI 事件,并相应地作出反应。在处理其中一个事件时完成的任何工作必须快速地进行,以便让 GUI 系统可以响应用户。

应该在单独的线程中执行由 UI 事件触发的长时间操作, 以便允许事件循环线程快速返回,并从应用程序的队列中访存下一个事件。然而,必须利用显式锁定和串行化来控制从其他线程访问小窗口和平台 API。未能遵循规则的应用程序可能会导致 OS 调用失败, 更糟糕的是,可能会锁定整个 GUI 系统。

工具箱 UI 线程

使用 C 语言的本机 GUI 程序员相当熟悉使用平台事件循环的设计注意事项。然而,用 Java 编写的许多较高级别的小窗口工具箱通常试图通过隐藏平台事件循环来向应用程序开发者隐瞒 UI 线程问题。

实现此过程的常见方法是设置专用工具箱 UI 线程,以便读取和调度事件循环, 并将事件送至由正在单独线程中运行的应用程序服务的内部队列中。 这允许工具箱有足够的时间来响应操作系统, 而不对应用程序处理事件的时间设置任何限制。应用程序必须仍然使用特殊的锁定技术来从它们的应用程序线程中访问 UI 代码, 但是,它在整个代码中的完成是一致的,原因是所有应用程序代码正在非 UI 线程中运行。

尽管听起来好象可以“防止”应用程序发生 UI 线程问题, 但是,实际上它将导致许多问题。

当 GUI 事件的时间取决于 Java 线程实现和应用程序性能时,就很难调试和诊断问题。

目前的 GUI 平台对事件队列执行了许多优化。常见的优化就是将多个绘制事件折叠到队列中。必须重新绘制窗口的每个时间部件,可以检查队列是否存在绘制事件重叠和尚未调度的冗余绘制事件。可以将这些事件合并到一个绘制事件中, 从而使得闪烁较少出现并且执行应用程序的绘制代码也不会太频繁。如果小窗口工具箱正在将事件快速拉出队列,并将它们发送到内部队列中,则此优化会失败。

改变开发者对线程模型的理解会导致程序员在使用其他语言和工具箱来为本机 GUI 系统编程时发生混淆。

SWT UI 线程

SWT 遵循平台直接支持的线程模型。应用程序在它的主线程中运行事件循环,并直接从此线程中调度事件。这是应用程序的“UI 线程”。

注意:从技术上来说,UI 线程是创建显示的线程。实际上,此线程也是运行事件循环和创建小窗口的线程。

由于所有事件代码都是从应用程序的 UI 线程中触发的, 因此,处理事件的应用程序代码可以自由地访问小窗口, 并且不需要任何特殊技术就可以进行图形调用。然而,当响应某事件而执行长时间运行的操作时, 应用程序负责创建计算线程的分支。

注意,对于所有来自非 UI 线程的调用,而这些调用又必须来自 UI 线程, SWT 将触发 SWTException

SWT 应用程序的主线程(包括事件循环)为如下所示:

public static void main(String [] args) {
    Display display = new Display();
    Shell shell = new Shell(display);
    shell.open();
    // 启动事件循环。我们会在用户完成调度窗口的操作
    // 时停止。
    while (!shell.isDisposed()) {
        if (!display.readAndDispatch())
            display.sleep();
    }
    display.dispose();
}

一旦创建了小窗口并且打开了外壳程序, 应用程序就会读取和调度 OS 队列中的事件,直到除掉外壳程序窗口为止。如果队列中没有事件可用,则说明显示进入睡眠状态,以使其他应用程序有机会运行。

注意:SWT 应用程序的最常见线程模型是在计算线程中运行单个 UI 线程并执行长时间操作。然而,SWT 不会限制开发者使用此模型。应用程序可以运行多个 UI 线程,而每个线程中都有单独的事件循环。

SWT 提供了特殊的访问方法来从后台线程中调用小窗口和图形代码。

执行非 UI 线程中的代码

希望从非 UI 线程中调用 UI 的应用程序必须提供调用 UI 代码的可运行程序显示类中的方法 syncExec(Runnable) asyncExec(Runnable) 用来在适当的时间在 UI 线程中执行这些可运行程序。

以下代码片段演示了使用这些方法的模式。

// 执行与时间有关的计算
...
// 现在更新 UI。我们不依赖结果,
// 所以使用异步。
Display.getCurrent().asyncExec(new Runnable() {
    public void run() {
        myWindow.redraw();
    }
});
// 现在执行其他计算
...

工作台和线程

当您从头实现 SWT 应用程序时,线程规则是很清楚的, 原因是由您控制事件循环的创建以及将应用程序中的计算线程分支的决定。

如果您正在向工作台添加插件代码,那么规则是什么?幸运的是,没有线程“幻数”隐藏在 JFace 或工作台代码中。规则是简单明了的。