线程(Thread)模型

对于每个桌面,事件总是被顺序处理的,所以线程模型是很简单的。就像开发桌面应用程序一样,你不需要担心racing和多线程(multi-threading)。所有你需要做的就是注册一个事件监听器且事件被调用时处理它。

[提示]:当一个ZUML页面在servlet线程中被赋值时,每一个事件监听器都在一个被称为事件处理线程的独立线程中运行。

[提示]:事件处理线程的使用可以被禁止,这样所有的线程都可以在Servlet线程中被处理。这样有更好一点的表现并且减少集成的问题。但是,你不能中止执行。参考性能提示一章中使用Servlet 线程处理事件一节。

挂起及恢复

为了高级的应用,你或许必须挂起一个执行直到一些条件被满足,org.zkoss.zk.ui.Executions类中的waitnotifynotifyAll方法都是为这样的目的而设计的。

当一个事件监听器想挂起自己,它可以调用wait。如果申请的具体条件得到满足,另一个线程可以通过notifynotifyAll方法唤醒它。对话框是使用这种机制的典型例子。

public void doModal() throws InterruptedException {
...
   Executions.wait(_mutex); //suspend this thread, an event processing thread
}
public void endModal() {
...
   Executions.notify(_mutex); //resume the suspended event processing thread
}

它们的使用类似于java.lang.Object中的waitnotifynotifyAll方法。但是,你不能使用java.lang.Object中的方法来挂起和恢复事件监听器。否则,关联到这个桌面的所有的事件处理都会停滞。

请注意,不同于Java的Object类的waitnotify方法,是否使用同步的synchronized block来包Executionswaitnotify是可选的。在上述情况下中,我们并不需要这样做,因为没有可能的racing问题。但是,如果存在这样的racing问题,你可以使用synchronized block,就像在Java Objectwaitnotify中使用那样。

//Thread 1
public void request() {
   ...
   synchronized (mutex) {
      ...//start another thread
      Executions.wait(mutex); //wait for its completion
   }
}

//Thread 2
public void process() {
   ... //process it asynchronously
   synchronized (mutex) {
      Executions.notify(mutex);
   }
}

长操作(Long Operations)

对于同一个桌面而言,事件是被顺序处理的。最坏的情况下,由于HTTP的限制,用户仅会看到显示在浏览器左上方的一个小处理对话框而没有其他任何提示。

通过使用上面回显事件章节描述的回显事件和showBusy方法,则可以提供更多描述性信息来提示用户接下来的行为,例如,点击其他按钮为进一步进行长操作降低性能。

但是,阻止来自用户的访问对你的应用程序而言也许是不可能的。为了防止阻塞,你必须在另一个线程中处理长操作,就像桌面应用程序那样。然后,持续将处理状态送回客户端。

使用ZK,你有四种选择:服务器推动(server push), 挂起和恢复(suspend and resume),timer, 和捎带(piggyback)。

选择1:服务器推动

服务器推动即所谓的反向Ajax(reverse-Ajax),允许服务器将内容动态的发至客户端。通过使用服务器推动技术,当你预先定义的条件满足时,则可以在工作线程内将内容发至客户端或更新客户端的内容。使用服务器推动很简单,仅需要如下的三步,

  1. 使用Desktop.enableServerPush(boolean bool)为桌面调用启用服务器推动。

  2. 将需要更新的组件传递至工作线程。

  3. 在桌面内调用工作线程。

[注]:你需要安装zkex.jarzkmax.jar来使用服务器推动,除非你有自己org.zkoss.zk.ui.sys.ServerPush的实现。

现在让我们来看一个实际的例子。若你想使用服务器推动更新客户端的数字,首先要为桌面启用服务器推动,然后调用线程 ,如下。

<window title="Demo of Server Push">
<zscript>
   import test.WorkingThread;
   WorkingThread thread;
   void startServerpush(){
      //enable server push
      desktop.enableServerPush(true);
         //invoke working thread and passing required component as parameter
         thread= new WorkingThread(info);
      thread.start();
   }
   void stopServerpush(){
      //stop the thread
      thread.setDone();
      //disable server push
      desktop.enableServerPush(false);
   }
</zscript>
   <vbox>
      <button label="Start Server Push" onClick="startServerpush()"/>
      <button label="Stop Server Push" onClick="stopServerpush()"/>
   <label id="info"/>
   </vbox>
</window>
安全问题

需要注意的一件事是同步问题,即当多个线程访问同一桌面时发生的问题。因此,在访问桌面前,你必须调用Executions.activate(Desktop desktop)来获取对桌面的完全控制权,以避免这个问题,在线程完成它的工作后调用Executions.deactivate(Desktop desktop)释放对桌面的控制,如下,

package	test;

public	class	WorkingThread extends Thread{
   private final Desktop _desktop;
   private final Label _info;
   private int _cnt;
   private boolean _ceased;
   public	WorkingThread(Label info){
      _desktop = info.getDesktop();
      _info	= info;
   }
   public	void	run(){
      try	{
         while	(!_ceased){
            Threads.sleep(2000); //Update each two seconds
            Executions.activate(_desktop); //get full control of desktop
            try	{
               _info.setValue(Integer.toString(++_cnt));
            }catch	(RuntimeException ex)	{
               throw ex;
            }catch	(Error ex){
               throw ex;
            }finally{
              Executions.deactivate(_desktop);//release full control of	desktop
            }
         }
      }	catch(InterruptedException ex){
         }
   }
   public	void	setDone(){
      _ceased = true;
   }
}
幕后

服务器推动机制是使用客户端轮询(client-polling)技术实现的,即客户端将会反复询问服务器以调用工作线程完成其工作,询问的频率可以调用Executions.setDelay(int min, int max, int factor)手动调整。

  1. min,为任何将要到来的服务器推动进程获得(poll)服务器的最小延迟。

  2. max,为任何将要到来的服务器推动进程获得(poll)服务器的最大延迟。

  3. factor,实际的延迟为多种延迟因素复合的处理时间。

最后,需要注意的一件事是频率会依赖服务器的加载而自动调整。

选择 2:线程挂起和恢复

使用服务器推动技术,你不需要关心多线程的问题。但是,若你想自己处理这个工作,由于HTTP的限制必须遵守如下的规则。

  1. 创建工作线程后,使用org.zkoss.zk.ui.Executions类的wait方法来挂起事件处理程序本身。

  2. 由于工作线程不是一个事件监听器,所以它不能读取任何组件,除非这个组件不属于任何桌面。因此在开始工作线程之前你可能需要手动添加(pass)一些必要信息。

  3. 然后,若有必要的话,工作线程可以取出信息,并且创建组件。只是不引用属于任何桌面的任何组件。

  4. 工作线程完成之后,在工作线程中使用org.zkoss.zk.ui.Executions类中的notify(Desktop desktop,Object flag)notifyAll(Desktop desktop,Object flag)方法来恢复事件处理程序。

  5. 直到另一个事件从客户端被发送过来,恢复的事件处理程序才会执行。为了强制发送一个组件,你可以使用timer组件(org.zkoss.zul.Timer)触发事件片刻之后或定期的。这个timer 的事件监听器可以不做任何事情或更改进展状况。

事例:一个异步产生标签的工作线程

假定我们以异步的方式创建标签。当然,用多线程来做这么小的事是没有意义的,但是我们可以用更复杂的(sophisticated)任务来代替这个。

//WorkingThread
package test;
public class WorkingThread extends Thread {
   private static int _cnt;

   private Desktop _desktop;
   private Label _label;
   private final Object _mutex = new Integer(0);

   /** Called by thread.zul to create a label asynchronously.
    * To create a label, it start a thread, and wait for its completion.
    */
   public static final Label asyncCreate(Desktop desktop)
   throws InterruptedException {
      final WorkingThread worker = new WorkingThread(desktop);
      synchronized (worker._mutex) { //to avoid racing
         worker.start();
         Executions.wait(worker._mutex);
         return worker._label;
      }
   }
   public WorkingThread(Desktop desktop) {
      _desktop = desktop;
   }
   public void run() {
      _label = new Label("Execute "+ ++_cnt);
      synchronized (_mutex) { //to avoid racing
         Executions.notify(_desktop, _mutex);
      }
   }
}

然后,在一个事件监听器中,我们使用ZUML页面来调用这个工作线程,如在onClick事件中。

<window id="main" title="Working Thread">
   <button label="Start Working Thread">
   <attribute name="onClick">
   timer.start();
   Label label = test.WorkingThread.asyncCreate(desktop);
   main.appendChild(label);
   timer.stop()
      </attribute>
   </button>
   <timer id="timer" running="false" delay="1000" repeats="true"/>
</window>

注意到我们必须使用timer来真正地恢复被挂起的事件监听器(onClick)。这看起来是多余的,但是由于HTTP的限制:为了保持Web页面在浏览器端的活跃,当事件处理程序被挂起时我们必须返回响应。然后,工作线程完成了工作并唤醒了事件监听器,HTTP请求已经不在了。因此,我们需要一种方式来”捎带(piggyback)”这个结果,而这就使timer为什么会被使用的原因。

更确切地说,当工作线程唤醒一个事件监听器时,ZK只是把它加到一个等待列表。当另一个HTTP请 求到达时,监听器才真正恢复(如上面例子中的onTimer事件)。

在这个简单的事例中,我们并没有为onTimer事件作任何事情。对于一个复杂的应用程序,你可以用它来返回处理状态。

选择 3: Timer(没有挂起/恢复)

无需挂起和恢复来实现一长操作(long operation)是由可能的。这在同步代码(synchronization codes)对于调试来说太复杂的情况下是很有用的。

注意是很简单的。工作线程将结果保存在一个临时空间,然后使用onTimer事件将结果弹到(pops)桌面。

//WorkingThread2
package test;
public class WorkingThread2 extends Thread {
   private static int _cnt;

   private final Desktop _desktop;
   private final List _result;

   public WorkingThread2(Desktop desktop, List result) {
      _desktop = desktop;
      _result = result;
   }   public void run() {
      _result.add(new Label("Execute "+ ++_cnt));
   }
}

然后,在onTimer事件监听器上附加标签。

<window id="main" title="Working Thread2">
   <zscript>
   int numPending = 0;
   List result = Collections.synchronizedList(new LinkedList());
   </zscript>
   <button label="Start Working Thread">
      <attribute name="onClick">
   ++numPending;   timer.start;
  new test.WorkingThread2(desktop, result).start();
      </attribute>
   </button>
   <timer id="timer" running="false" delay="1000" repeats="true">
      <attribute name="onTimer">
   while (!result.isEmpty()) {
      main.appendChild(result.remove(0));
      --numPending;
   }
   if (numPending == 0) timer.stop();
      </attribute>
   </timer>
</window>

选择 4:捎带(piggyback)(没有挂起/恢复,没有Timer)

当用户,例如,点击一个按钮或输入一些东西时,你可以将结果捎带(piggyback)到客户端,而不必循环地检查它们。

为了完成捎带(piggyback),你需要为一个根组件注册一个onPiggyback事件监听器。然后每次ZK更新引擎(ZK Update Engine)处理事件时,这个监听器将会被调用。例如,你可以按如下的方式重写代码。

<window id="main" title="Working Thread3" onPiggyback="checkResult()">
   <zscript>
   List result = Collections.synchronizedList(new LinkedList());

   void checkResult() {
      while (!result.isEmpty())
         main.appendChild(result.remove(0));
   }
   </zscript>
   <button label="Start Working Thread">
      <attribute name="onClick">
   timer.start();
   new test.WorkingThread2(desktop, result).start();
      </attribute>
   </button>
</window>

捎带(piggyback)方式的优点是客户端与服务器端没有额外的往来。但是,如果用户没有任何活动(如点击或打字)的话是无法看到更新的。这种方式是否合适取决于应用程序的要求。

[注]: 一个延期的事件(deferrable)不会马上被送到客户端,所以,只有一个非延期的事件被触发后onPiggyback事件才会被触发。请参考关于延期事件监听器一节获得详细信息。