在J2EE中使用 Work Manager 規範執行並行任務

作者:Dmitri Maximovich 時間:2005-11-23 19:53 出處:bea

 

到目前爲止,人們還沒有發現一種途徑可以方便地在 J2EE 應用程序中啓動執行並行任務。設想您的應用程序需要處理多個客戶端請求(不管是在 servlet 中還是在 session bean 中),而如果這些請求以並行的方式執行,其效率可能更高。作爲一個簡單例子,我們假設這樣一種情況,即客戶請求可以通過從多個 URL、web服務或 SQL 查詢語句獲得數據而得到處理,並且執行順序不重要。這些用例非常需要以並行的方式執行;它們花費絕大部分時間來等待響應,因此它們佔用的處理能力並不多。除此之外,因爲有很多已經實現了,因此強健的併發設計是可擴展軟件的重要組成部分。

  本文研究的是在 J2EE 中使用 Work Manager 規範來執行並行任務。我們創建了一個可實行的例子,來說明如何使用 Work Manager 規範在 servlet 容器中實現任務的並行執行。

J2EE 中的併發

  爲了在J2EE 容器中實現並行任務的執行,我們有哪些選擇?奇怪的是,方法並不多。您可能也知道,J2EE 規範禁止在運行在 J2EE 容器中的受控代碼中創建線程。(在實踐中,這個規則很少得以貫徹執行,甚至還有些組件因爲破壞此規則而被人們熟知,例如Quartz sheduler。)然而,J2EE 規範根本沒有討論並行執行,因此,需要實現並行執行的開發人員只有依靠他們自己。

  在實踐中,一般來說,如果要與 J2EE 規範兼容,您只有一個可用的選擇:使用隊列將同步調用拆分成多個異步任務。這種方法可以解決問題,但是實現起來相當複雜;它要求定義目標、創建消息並編寫MDB代碼。更糟的是,如果您承擔不起丟失那些可能需要訪問另外的資源管理器的消息,還必須使用持久隊列。(一個例子是,代碼在 WebLogic 平臺上運行,但卻需要使用 IBM MQ 來排隊。從這裏開始,這是到 XA 事務的一個步驟。)

  另外一個困難隨之產生,即在繼續執行主程序流之前,您需要等待一組異步子任務的完成。您不久就會看到,使用新的 Work Manager 框架能方便地實現並行執行。

適合應用程序服務器規範的 Work Manager

  所有這些複雜性似乎很快就會成爲過去,因爲 J2EE 服務器市場的兩大供應商IBM 和 BEA 正在聯合研發一種規範,該規範爲任務的併發執行提供簡單的、容器可管理的編程模型。該行動被稱爲CommonJ,其中一個部分是Work Manager for Application Servers規範,即現在可得到的JSR 237。該規範在2003年首次公佈,而IBM 的 WebSphere Application Server 自企業版5.0就開始支持任務的並行執行,該版本是編程模型擴展(Programming Model Extensions,PME)的一部分。在 WebSphere 文檔中,該功能有時被稱爲“異步 bean”。在 WebSphere 6.0中,所有的版本都會支持併發執行。Bea Weblogic Server 在BEA WebLogic Server 9.0中提供對應的功能。有了 BEA 和 IBM 的支持,該規範在不久的將來極有可能至少成爲事實上的標準。

使用 Work Manager 功能

  讓我們來看看如何開始使用這個新功能。下面這個例子是在 WebLogic Server 9.0 beta 版本上測試的。爲了簡單起見,我們在這個例子中使用一個 servlet 作爲執行起點,但是如果您的應用程序入口點是一個會話 bean 或者消息驅動bean,其邏輯也是適用的。

  我們假設一個虛構的用例,即一個 servlet 接收了一個單詞列表,需要將這些單詞從一種語言翻譯成另一種語言(例如,通過使用 Google 的語言工具或某些類似的遠程服務)。這些單詞可能來自於 HTML 頁面中的多重選擇列表。servlet 代碼在向客戶端返回結果之前,必須翻譯所有單詞。我們不會爲了此練習而編寫代碼來實現真正的翻譯,我們真正要做的是使用具有可配置延遲的啞翻譯器,來模擬對翻譯服務的遠程調用。

Translator 接口及其實現

  讓我們來爲Translator定義接口:

public interface Translator {
public String getSource();
public void translate();
public String getTranslation();
}

  您會看到,Translator 被設計爲“有狀態”(stateful)。假定您的代碼需要創建Translator 實例,將單詞作爲構造器調用的參數傳遞從而翻譯,然後調用translate()方法,最終通過調用 getTranslation()方法獲得翻譯後的單詞。辨證地說,對於翻譯任務來說,這種設計並非是最佳的。而且在這個例子中,很可能只需要使用一個單獨的字符串 translate(字符串) 方法就足以應付了。但是,您很快就會看到,當以並行方式執行多個翻譯時,它會和Work接口結合得很好。對我們的例子來說,我們將使用一個非常簡單的DummyTranslator 實現。但是首先我們要定義AbstractTranslator來封裝在各種實現中常見的域和方法。

public abstract class AbstractTranslator implements Translator {
protected final String source;
protected String translation;
public AbstractTranslator(String source) {
this.source = source;
}
public String getSource() {
return this.source;
}
public String getTranslation() {
return this.translation;
}
public String toString() {
return "source="+getSource()+", translation="+getTranslation();
}
}

  現在,任何的具體實現只需要執行translate()方法:

public class DummyTranslator extends AbstractTranslator {
private final long delay;
public DummyTranslator(String source, long delay) {
super(source);
this.delay = delay;
}
public void translate() {
// delay to simulate network call
try {
Thread.sleep(this.delay);
}
catch (InterruptedException ignore) { }
this.translation = this.source+"_tr";
}
}

  現在我們已經準備好實現我們的 servlet 了,首先我們針對慣用的串行執行,然後再針對並行任務執行。

串行實現

  爲了簡單起見,我們首先創建一個AbstractServlet,它封裝所有有關HttpServlet 的代碼,並且將真正的翻譯工作委託給子類(策略模式)。

public abstract class AbstractServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException  {
logger.info("begin");
response.setContentType("text/plain");
PrintWriter writer = resp.getWriter();
long start = System.currentTimeMillis();
List input = getClientInput();
List result = null;
try {
result = doTranslation(input);
}
catch (Exception e) {
throw new ServletException(e);
}
long stop = System.currentTimeMillis();
logger.info("done in "+(stop-start));
writer.print("done in "+(stop-start)+"/n"+result);
}
// actual translation logic goes here
protected abstract List doTranslation(List input) throws Exception;
// let's hardcode list for our test
// in real life this method should extract parameters from HttpServletRequest
private List getClientInput() {
return Arrays.asList(new String[]{"one", "two", "three", "four", "five"});
}
}

  最後,這就是串行執行的代碼:

public class ServletSequential extends AbstractServlet {
private static final long DELAY = 10 * 1000; // 10 sec
protected List doTranslation(List input) {
List result = new ArrayList();
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslator(source, DELAY);
translator.translate();
result.add(translator.getTranslation());
}
return result;
}
}

  當這個 servlet 執行時,它會花費50秒來“翻譯”我們將其作爲一個參數的5個單詞(每次調用DummyTranslator.translate() 花費10秒鐘)。讓我們來研究一下如何重寫這個例子,以利用WorkManager的功能。

修改代碼以並行執行

  要修改代碼以利用並行執行,第一步是修改Translator來實現Work接口。爲此有兩種方式:可以修改AbstractTranslator 來實現 Work,以補充 Translator 接口;或者可以提供某種包裝類(wrapper class),它們可以使用 Translator 實例來實現Work,即代理方法調用。我們將採用第二種方法,它允許我們無需修改就可以重用已有的DummyTranslator。

public class WorkTranslatorWrapper implements Translator, Work {
private final Translator impl;
public WorkTranslatorWrapper(Translator impl) {
this.impl = impl;
}
public String getSource() {
return this.impl.getSource();
}
public String getTranslation() {
return this.impl.getTranslation();
}
public void translate() {
this.impl.translate();
}
public void release() {
}
public boolean isDaemon() {
return false;
}
public void run() {
translate();
}
public String toString() {
return this.impl.toString();
}

  正如您看到的,WorkTranslatorWrapper爲定義在 Translator 接口中的所有方法提供了實現,它作爲一個構造器參數而傳遞,爲到達具體的Translator 實現代爲代理這些方法。爲了滿足 Work 接口的需要,我們定義了三個新的方法(Work 接口反過來繼承了Runnable)。run()方法是 Work 執行的入口點;在本例中我們只是將它重定向到translate() 方法。release()方法可以用來設置任何變量來終止 run()方法中的主循環(如果有的話),然後幾乎以與Java 線程中推薦的完全相同的方式返回。如果 Work 能夠比調度它的 servlet 請求或者 EJB 方法堅持得更久,isDaemon() 方法應該返回true。如果返回了 false,Work持續時間通常不能長於提交容器方法的時間。如果使用的資源在方法的持續期裏纔有效,那麼該提交方法(submitting method)應該一直等到暫活的(short-lived)(non-daemon)工作完成。對我們的例子來說,採用non-daemon是正確的方法,因爲我們要等到所有的結果都可用才向客戶端返回結果。

  既然實現了 Work,我們就可以重寫 servlet 代碼來使用WorkManager了,但首先我們需要在容器中定義WorkManager。

在容器中定義 WorkManager

  Work Manager 其實是容器資源,就像 JMS 隊列和連結池。您可以使用Administration Console來配置這些組件。其實, WorkManager 就是一個帶有容量參數的線程池,其中有可分配的最大線程數目和最小線程數目、響應時間策略等等。不過,所有這些選項在 beta 版本中都是無法通過控制檯來修改的,因此,所有的配置歸結爲爲新建的 WorkManager 提供一個名稱。在本例中,我們使用MyWorkManager。


圖1. 在 WebLogic 9.0上配置WorkManager

  當完成配置後,重新啓動 WebLogic Sever,啓動時,您就會發現(要麼在控制檯要麼在日誌文件裏面,這依賴於您的日誌設置)已經爲應用程序創建了 MyWorkManager 。

<Mar 31, 2005 5:42:47 PM EST> <WorkManager> <Creating WorkManager from "MyWorkManager" WorkManagerMBean for application "workman.war">

  現在,剛剛定義的MyWorkManager就存在於 JNDI 樹中了。該容器可以支持任意數量的獨立WorkManager實例。獲得一個WorkManager實例的主要方法是在本地 Java 環境中使用一個 JNDI 搜索(即:java:comp/env/wm/[work manager名稱])。因此,Work Manager 是在部署時配置的,其方法是使用部署描述符作爲 resource-ref。對特定 Work Manager(例如,wm/MyWorkManager)的每次 JNDI 搜索均返回該WorkManager的一個共享實例。因爲WorkManager是線程安全(thread-safe)的,所以查詢就可以一次性完成,通常在 init()方法(servlet)或ejbCreate()方法(session EJB)中實現。

  推薦的配置方法是在相應的標準部署描述符web.xml (對於EJB,是ejb-jar.xml)中定義resource-ref:

 <web-app>
...
<resource-ref>
<res-ref-name>wm/MyWorkManager</res-ref-name>
<res-type>commonj.work.WorkManager</res-type>
<res-auth>Container</res-auth>
<res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>
...
</web-app>

  同樣,也在特定於 WebLogic 的部署描述符weblogic.xml (或weblogic-ejb-jar.xml)中定義resource-ref。

  <weblogic-web-app>
...
<reference-descriptor>
<resource-description>
<res-ref-name>wm/MyWorkManager</res-ref-name>
<jndi-name>MyWorkManager</jndi-name>
</resource-description>
</reference-descriptor>
...
</weblogic-web-app>

修改 servlet 來使用並行執行

  下面就是ServletParallel 類的代碼,它繼承了 AbstractServlet 類。請注意對WorkManager的搜索在 init() 方法中是如何實現的。代碼的大體結構與ServletSequential 非常相似:doTranslation()方法包含了與輸入列表相同的循環,但是不是直接執行 Translator,而是創建了一個 WorkTranslatorWrapper實例,然後通過該實例調用WorkManager 的schedule() 方法。調用的schedule()方法立即返回,我們需要保存作爲結果的 WorkItem(通過將它添加到任務列表中)。在所有的任務都安排好後,執行會在調用WorkManager.waitForAll(Collection, long)時阻塞,該方法使用 WorkItem集合用於我們要等待的任務。第二個參數指定以毫秒爲單位的超時,我們有兩個預定義的常量: WorkManager.IMMEDIATE,它用來指定方法應該立即返回(它與傳遞“0”是同樣的效果); WorkManager.INDEFINITE,它表明沒有超時一直要等到所有任務都完成。

public class ServletParallel extends AbstractServlet {
private WorkManager workManager;
public void init(ServletConfig servletConfig) throws ServletException {
try {
InitialContext ctx = new InitialContext();
this.workManager = (WorkManager)ctx.lookup("java:comp/env/wm/MyWorkManager");
}
catch (Exception e) {
throw new ServletException(e);
}
}
protected List doTranslation(List input) throws Exception {
List result = new ArrayList();
List jobs = new ArrayList();
// create translators and schedule execution
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslator(source, 10 * 1000);
// schedule
Work work = new WorkTranslatorWrapper(translator);
WorkItem workItem = this.workManager.schedule(work);
jobs.add(workItem);
}
logger.info("All jobs scheduled");
// wait for all jobs to complete
this.workManager.waitForAll(jobs, WorkManager.INDEFINITE);
// extract results
for (Iterator iter = jobs.iterator(); iter.hasNext();) {
WorkItem workItem = (WorkItem) iter.next();
Translator translator = (Translator) workItem.getResult();
result.add(translator.getTranslation());
}
return result;
}
}

  請注意當任務完成時結果是如何抽取的:我們在WorkItem 集合上循環調用 getResult()方法,該方法返回相應任務的實例。在本例中,這些可以拋給Translator。

  該 servlet 如果用和ServletSequential 相同的輸入參數執行,那麼就會以明顯快的速度完成執行(回想一下:在順序執行的情況下,耗時50秒)。在我的計算機上,執行大約耗時25秒到30秒。但是,結果肯定會隨所使用的特定WorkManager的配置情況、服務器負載以及其他因素而有所不同。WebLogic Server 9同樣也優化了線程的使用,並在 Work Manager 之間共享它們。此外,它確保請求得到公平處理。

Work 的生命週期和生命週期事件

  現在,讓我們更加仔細地研究 Work Manager 規範提供的功能。每個 Work 實例都有一個明確定義的生命週期。它定義瞭如下的狀態:

  • ACCEPTED接受——定義爲WorkEvent.WORK_ACCEPTED的常量,表明Work已被接受,可以調度了。
  • REJECTED拒絕——定義爲WorkEvent.WORK_REJECTED的常量,表明已接受的Work無法啓動(很可能是因爲WorkManager或應用服務器本身的問題)
  • STARTED開始——定義爲WorkEvent.WORK_STARTED的常量,表明Work已經開始執行。
  • COMPLETED完成——定義爲WorkEvent.WORK_COMPLETED的常量,表明Work已經完成執行。


圖 2. Work 的狀態圖

  您可以隨時調用WorkItem.getStatus()方法檢索經過調度的 Work 的當前狀態。當您不想等待所有任務的完成時,這個功能尤其有用。如果您對任何任務的完成感興趣,您可以使用WorkManager.waitForAny(Collection, timeout)方法,或者在一個循環中調用 Thread.sleep(long)方法,並且可以通過迭代整個 WorkItem 集合並檢查單個的任務狀態來鑑定有多少任務已經完成了。

  這個規範也提供了當 Work 實例改變它們的生命週期狀態時通知應用程序的方法。當work正在調度時,可以指定一個WorkListener。WorkManager要爲各種 work 事件(例如,接受、拒絕、開始、完成)回調WorkListener實例。請注意,WorkListener 實例始終與使用WorkManager調度work 的線程在相同的 Java 虛擬機(JVM)中執行。WorkListener 類可以以獨立類的形式實現,或者以 Work 類的一部分的形式來實現。下面就是listener的一個簡單實現,當不同的事件發生時,它會記錄一條消息。

public class TranslatorWorkListener implements WorkListener {
public void workAccepted(WorkEvent workEvent) {
logger.info("work accepted: "+workEvent.getWorkItem());
}
public void workRejected(WorkEvent workEvent) {
logger.info("work rejected: "+workEvent.getWorkItem());
}
public void workStarted(WorkEvent workEvent) {
logger.info("work started: "+workEvent.getWorkItem());
}
public void workCompleted(WorkEvent workEvent) {
logger.info("work completed: "+workEvent.getWorkItem());
}
}

  然後可以更改上述ServletParallel代碼中的doTranslation()實現,來將一個TranslatorListener 傳遞到 WorkManager:

protected List doTranslation(List input) throws Exception {
...
TranslatorWorkListener listener = new TranslatorWorkListener();
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslator(source, 10 * 1000);
// schedule
Work work = new WorkTranslatorWrapper(translator);
WorkItem workItem = this.workManager.schedule(work, listener);
jobs.add(workItem);
}
logger.info("All jobs scheduled");
...
}

  如果您運行修改後的代碼,您會在日誌文件(或者控制檯,這依賴於 WebLogic 以及您的日誌記錄器的配置情況)中看到與下面內容相似的信息:

22:42:12 - begin
22:42:12 - work accepted: executing: source=one, translation=null
22:42:12 - work accepted: executing: source=two, translation=null
22:42:12 - work accepted: executing: source=three, translation=null
22:42:12 - work accepted: executing: source=four, translation=null
22:42:12 - work accepted: executing: source=five, translation=null
22:42:12 - All jobs scheduled
22:42:20 - work started: executing: source=one, translation=null
22:42:24 - work started: executing: source=two, translation=null
22:42:28 - work started: executing: source=three, translation=null
22:42:30 - work started: executing: source=four, translation=null
22:42:30 - work completed: executing: source=one, translation=one_tr
22:42:30 - work started: executing: source=five, translation=null
22:42:34 - work completed: executing: source=two, translation=two_tr
22:42:38 - work completed: executing: source=three, translation=three_tr
22:42:40 - work completed: executing: source=four, translation=four_tr
22:42:40 - work completed: executing: source=five, translation=five_tr
22:42:40 - done in 27641

  在上述定義的TranslatorWorkListener中有個小竅門;它只是將 WorkItem 打印到日誌中,並且因爲重寫了AbstractTranslator 中的 toString()方法,就看到所有那些“source=one, translation=null”的代碼行,它們識別特定的任務。事實上,如果需要在WorkListener 中將 WorkItem 與 Work 對象關聯,您必須將每個 WorkItem 傳遞到該監聽器(listener),並且要保存它,以便匹配從 WorkEvent 獲得的WorkItem。(getResult()方法調用workEvent.getWorkItem()時會一直返回null,直到任務變成COMPLETED狀態。)下面的代碼闡明瞭這個技巧:

public class TranslatorWorkListener implements WorkListener {
protected Map workMap =
Collections.synchronizedMap(new HashMap());
public void workAccepted(WorkEvent workEvent) {
logger.info("work accepted: "
+getTranslator(we.getWorkItem()).getSource());
}
public void workRejected(WorkEvent workEvent) {
logger.info("work rejected: "
+removeTranslator(we.getWorkItem()).getSource());
}
public void workStarted(WorkEvent workEvent) {
logger.info("work started: "
+getTranslator(we.getWorkItem()).getSource());
}
public void workCompleted(WorkEvent workEvent) {
logger.info("work completed: "
+removeTranslator(we.WorkItem()).getSource());
}
public void addTranslator(WorkItem wi, Translator t) {
workMap.put(wi, t);
}
public Work getTransaltor(WorkItem wi) {
return (Translator)workMap.get(wi);
}
public Work removeTranslator(WorkItem wi) {
return (Translator)workMap.remove(wi);
}
}

  爲了使用這個方法,要修改 servlet 代碼,在調用 schedule() 方法之後緊跟着就調用TranslatorWorkListener.addTranslator(WorkItem, Translator)方法。

異常處理

  到目前爲止,我們都假設我們的任務始終成功地執行,並且從不拋出異常。現實中,情況當然並不總是如此。在我們虛構的用例中,就可能會出現使翻譯不能成功執行的網絡問題,或者單詞可能拼寫錯誤或從字典中找不到。當我們從 Work 對象的 run()方法中拋出一個異常時,會怎麼樣?首先,注意 run()方法的簽名並沒有定義任何已檢查的異常,因此,我們必須使用RuntimeException 的一個實例,或者使用它的任意子類。爲了進一步討論異常的使用,讓我們首先定義一個TranslationException類:

public class TranslationException extends RuntimeException {
public TranslationException(String message) {
super(message);
}
public TranslationException(String message, Throwable cause) {
super(message, cause);
}
}

現在,我們還要創建Translator 的另一個實現,它會在50%的案例中隨機地拋出一個TranslationException(異常)。

public class DummyTranslatorWithError extends AbstractTranslator {
private final long delay;
public DummyTranslatorWithError(String source, long delay) {
super(source);
this.delay = delay;
}
public void translate() {
// delay to simulate network call
try {
Thread.sleep(this.delay);
}
catch (InterruptedException ignore) { }
// randomly throw Exception
if (Math.random() > 0.5) {
throw new TranslationException("Cannot translate "+getSource());
}
this.translation = this.source+"_tr";
}
}

  如果在ServletParallel的實現中將DummyTranslator替換爲DummyTranslatorWithError,並且運行它,有時就會在返回結果時得到異常。所發生的情況是,容器截獲任何從任務拋出的異常,並且當調用WorkItem.getResult() 時再次拋出該異常。我們可以使用這種知識來修改ServletParallel實現,來容納錯誤處理:

protected List doTranslation(List input) throws Exception {
...
TranslatorWorkListener listener = new TranslatorWorkListener();
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslatorWithError(source, 10 * 1000);
// schedule
Work work = new WorkTranslatorWrapper(translator);
WorkItem workItem = this.workManager.schedule(work, listener);
jobs.add(workItem);
}
logger.info("All jobs scheduled");
this.workManager.waitForAll(jobs, WorkManager.INDEFINITE);
// extract results
for (Iterator iter = jobs.iterator(); iter.hasNext();) {
WorkItem workItem = (WorkItem) iter.next();
try {
// if the Work threw an exception during run
// then the exception is rethrown here
Translator translator = (Translator) workItem.getResult();
result.add(translator.getTranslation());
}
catch (Exception e) {
result.add(e.getMessage());
}
}
return result;
}

  請注意,如果 Work 在執行時期拋出一個異常,那麼就不能通過執行WorkItem.getResult()調用來獲得原始的 Work 實現。如果需要將失敗的任務與原始Translator 關聯,該應用程序可以維護一個 WorkItem對象與Translator對象之間的Map。

安全性和事務上下文傳播

  Work Manager 規範的當前版本(1.1)不包括從調用者線程向調度任務的安全性和事務上下文傳播,這也沒什麼。在當前所有的實現中,安全性上下文是傳播的,而事務上下文則不然,而它是有意義的,只要您想想,假如一個應用程序開始了一個新的事務,調度任務,然後提交事務而不等待任務完成,會怎麼樣呢?另一方面,任務可以自主控制開始新事務、提交事務或者回滾該事務。任務也可以從對象的父類 JNDI comp:namespace 中查詢對象。

結束語

  在本文中,我們創建了一個可實行的例子,使用 Work Manager 規範在 servlet 容器中實現任務的並行執行。我們上面討論的所有內容在 EJB 容器中也適用。現在,隨着BEA WebLogic 9.0的即將發佈,開發人員有了一套方便、簡單而強大的 API,它能從主執行線程啓動和運行任意數目的並行進程,同時具備一個靈活的同步機制,並支持生命週期事件。

  我們的討論沒有涵蓋該規範的所有方面,甚至對 WorkManger 領域的討論也不詳盡。例如,還有另一種支持遠程任務執行的方法。如果 WorkManager 實現支持 Remoteable WorkManager,那麼就可以將 Work 發送到應用程序集羣的某個遠程成員以執行。目前 WebLogic 或 WebSphere 平臺還不支持該功能,不過如果要求負載平衡的話,將很有希望出現這個功能。

  此外,該規範還支持 Timer(計時器),這比現有的 JMX 中的timer規範和 EJB 2.1中的timer service更加靈活。我們希望在後續文章中討論這個問題。

其他讀物

原文出處

http://dev2dev.bea.com/pub/a/2005/05/parallel_tasks.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章