Eclipse用戶端程式中多線程的使用

來源:互聯網
上載者:User

Eclipse作為一個開發平台,使用越來越廣泛,基於Eclipse Rich Client Platform開發的用戶端程式也越來越多。在當今越來越複雜的應用環境中,我們的用戶端程式不可避免的要同時進行多任務的處理。一個優異的用戶端程式都會允許使用者同時啟動多個任務,從而大大提高使用者的工作效率以及使用者體驗。本文中我們來談談Eclipse中實現多任務的方式。

在我們基於Eclipse的Java程式中,我們有很多種方式提供多任務的實現。熟悉Java的朋友立即會想到Java的Thread類,這是Java中使用最多的一個實現多任務的類。Eclipse平台為多任務處理提供了自己的API,那就是Job以及UIJob。Eclipse中的Job是對Java Thread的一個封裝,為我們實現多任務提供了更方便的介面。以下是Job的基本用法:

清單 1. Job用法樣本

                Job job = new Job(“Job Name”){protected IStatus run(IProgressMonitor monitor) {// 在這裡添加你的任務代碼return Status.OK_STATUS;}};job.schedule(delayTime);

在Eclipse中我們也會經常用到Display.asynchExec() 和Display.synchExec()來啟動任務的執行。這兩個方法主要為了方便我們完成介面操作的任務。以下是Display.asynchExec()的用法,Display.synchExec()和它類似。

清單 2. Display.synchExec()用法樣本

                Display.getDefault().asyncExec(new Runnable() {public void run() {// 在這裡添加你的任務代碼}});

通常,在Eclipse中我們最好使用Eclipse提供的Job介面來實現多任務,而不是使用Java的thread。為什麼呢?主要有以下幾個原因:

  • Job是可重用的工作單元,一個Job我們可以很方便的讓它多次執行。
  • Job提供了方便的介面,使得我們在處理中能夠很方便的與外界交流,報告當前的執行進度
  • Eclipse提供了相應的機制使得程式員可以方便的介入Job的調度,例如我們可以方便的實現每次只有一個同一類型的Job在運行
  • Eclipse預設提供了Job管理的程式,可以查看當前所有的Job和它們的進度,也提供UI終止、暫停、繼續指定的Job
  • 使用Job可以提高程式的效能,節省線程建立和銷毀的開銷。Eclipse中的Job封裝了線程池的實現。當我們啟動一個Job時,Eclipse不會馬上建立一個Thread,它會在它的線程池中尋找是否有閒置線程,如果有空閑線程,就會直接用空閑線程運行你的Job。一個Job終止時,它所對應的線程也不會立即終止,它會被返回到線程池中以備重複利用。這樣,我們可以節省建立和銷毀線程的開銷

下面我們從幾個方面來討論Eclipse中Job的實現和使用方面的問題。

Eclipse中Job的實現

Eclipse的核心包中提供了一個JobManager類,它實現了IJobManager介面,Eclipse中Job的管理和調度都是由JobManager來實現的。JobManager維護有一個線程池,用來運行Job。當我們調用Job的schedule方法後,這個Job會被JobManager首先放到一個Job啟動並執行等待隊列中去。之後,JobManager會通知線程池有新的Job加入了運行等待隊列。線程池會找出一個閒置線程來運行Job,如果沒有空閑線程,線程池會建立一個新的線程來運行Job。一旦Job運行完畢,運行Job的線程會返回到線程池中以備下次使用。 從上面Job啟動並執行過程我們可以看到,JobManager介入了一個Job啟動並執行全過程,它瞭解Job什麼時候開始,什麼時候結束,每一時候Job的運行狀態。JobManager將這些Job啟動並執行資訊以介面的方式提供給使用者,同時它也提供了介面讓我們可以介入Job的調度等,從而我們擁有了更加強大的控制Job的能力。

為了我們更方便的瞭解Job所處的狀態,JobManager設定Job的一個狀態標誌位,我們可以通過Job的getState方法獲得Job當前的狀態值以瞭解其狀態:

  • NONE:當一個Job剛構造的時候,Job就會處於這種狀態。當一個Job執行完畢(包括被取消)後,Job的狀態也會變回這種狀態。
  • WAITING:當我們調用了Job的shedule方法,JobManager會將Job放入等待啟動並執行Job隊列,這時Job的狀態為WAITING.
  • RUNNING:當一個Job開始執行,Job的狀態會變為RUNNING。
  • SLEEPING:當我們調用Job的sleep方法後,Job會變成這一狀態。當我們調用schudule方法的時候帶上延時的參數,Job的狀態也會轉入這一狀態,在這一段延時等待的時間中,Job都處於這一狀態。這是一種睡眠狀態,Job在這種狀態中時不能馬上轉入運行。我們可以調用Job的wakeup方法來將Job喚醒。這樣,Job又會轉入WAITING狀態等待運行。

Eclipse中的UI線程

另外,在Eclipse的線程處理中,有一個UI線程的概念。Eclipse程式中的主線程是一個特殊的線程,程式啟動後會先執行這個線程,也就是我們的main()函數所在的線程。作為傳統型應用程式,我們的主線程主要負責介面的響應以及繪製介面元素,所以通常我們也叫它UI線程。

以下代碼,編過SWT應用程式的讀者會非常熟悉。它一般出現在main函數的結尾。下面來仔細分析一下它的詳細情況。

//當視窗未釋放時while (!shell.isDisposed()) {    //如果display對象事件隊列中沒有了等待的事件,就讓該線程進入等待狀態    if (!display.readAndDispatch())        display.sleep();}

上面的程式實際上就是我們UI線程的處理邏輯:當程式啟動後,UI線程會讀取事件等待隊列,看有沒有事件等待處理。如果有,它會進行相應處理,如果沒有它會進入睡眠狀態。如果有新的事件到來,它又會被喚醒,進行處理。UI線程所需要處理的事件包括使用者的滑鼠和鍵盤操作事件,作業系統或程式中發出的繪製事件。一般來說,處理事件的過程也就是響應使用者操作的過程。

一個好的傳統型應用程式需要對使用者的操作作出最快的響應,也就是說我們的UI線程必須儘快的處理各種事件。從我們程式的角度來說,在UI線程中我們不能進行大量的計算或者等待,否則使用者操作事件得不到及時的處理。通常,如果有大量的計算或者需要長時間等待(例如進行網路操作或者資料庫操作)時,我們必須將這些長時間處理的程式單獨開闢出一個線程來執行。這樣雖然後台運行著程式,但也不會影響介面上的操作。

除主線程之外的所有線程都是非UI線程。 在Eclipse程式中,我們所有對介面元素的操作都必須放到UI線程中來執行,否則會拋出Exception,所以我們要區分出UI線程和非UI線程,保證我們對UI的操作都在UI線程中執行。

如何判斷當前線程是否UI線程:你可以通過調用Display.getCurrent()來知道當前線程是否是UI線程。如果Display.getCurrent()返回為空白,表示當前不是UI線程。

Eclipse中使用線程的幾種典型情況

  • 控制Job的並發運行

對於某些Job,為了避免並發性問題,我們希望同時只有一個這樣的Job在運行,這時我們需要控制Job的並發運行。在另一種情況下,我們也需要控制Job的並發運行:我們在程式中對於一個任務,我們有可能會啟動一個Job來執行,對於少量的任務來說,這是可行的,但是如果我們預測可能會同時有大量的任務,如果每一個任務啟動一個Job,我們同時啟動的Job就會非常多。這些Job會侵佔大量的資源,影響其他任務的執行。 我們可以使用Job的rule來實現控制Job的並發執行。簡單的我們可以通過下面的代碼實現。我們先定義一個如下rule:

private ISchedulingRule Schedule_RULE = new ISchedulingRule() {public boolean contains(ISchedulingRule rule) {return this.equals(rule);}public boolean isConflicting(ISchedulingRule rule) {return this.equals(rule);}};

對於需要避免同時啟動並執行Job,我們可以將它們的rule設成上面定義的rule。如:

myjob1.setRule(Schedule_RULE);myjob2.setRule(Schedule_RULE);

這樣對於myjob1和myjob2這兩個Job,它們不會再同時執行。Myjob2會等待myjob1執行完再執行。這是由Eclipse的JobManager來提供實現的。JobManager可以保證所有啟動的Job中,任意兩個Job的rule是沒有衝突的。 我們在上面定義的rule是最簡單的。我們可以重寫isConflicting函數來實現一些更加複雜的控制,比如控制同時同類型的Job最多隻有指定的個數在運行。 但是我們要注意,isConflicting方法不能過於複雜。一旦一個Job的rule與其他Job的rule有衝突,isConflicting方法會調用很多次。如果其中的計算過於複雜,會影響整體的效能。

  • 根據需要執行Job

由於我們有的Job有可能不是立即執行的,在有些情況下,等到該Job準備執行的時候,該Job所要執行的任務已經沒有意義了。這時,我們可以使用Job的shouldSchedule()和shouldRun()來避免Job的運行。在我們定義一個Job時,我們可以重載shouldSchedule和shouldRun方法。在這些方法中,我們可以檢查Job啟動並執行一些先決條件,如果這些條件不滿足,我們就可以返回false。JobManager在安排Job運行時,它會先調用該Job的shouldSchedule方法,如果返回為false,JobManager就不會再安排這個Job運行了。同樣,JobManager在真正啟動一個線程運行一個Job前,它會調用該Job的shouldRun方法,如果返回false,它不再運行這個Job。在下面的例子中,我們希望啟動一個Job在十秒鐘之後更新文字框中的內容。為了保證我們的Job運行時是有意義的,我們需要確保我們要更新的文字框沒有被銷毀,我們重載了shouldSchedule和shouldRun方法。

Text text = new Text(parent,SWT.NONE);UIJob refreshJob = new UIJob(“更新介面”){public IStatus runInUIThread(IProgressMonitor monitor) {text.setText(“新文本”);return Status.OK_STATUS;}public boolean shouldSchedule(){return !text.isDisposed();}public boolean shouldRun(){return !text.isDisposed();}};refreshJob.schedule(10000);

  • 在UI線程中涉及長時間處理的任務

我們經常碰到這樣一種情況:使用者操作菜單或者按鈕會觸發查詢大量資料,資料查詢完後更新表格等介面元素。使用者點擊菜單或者按鈕所觸發的處理常式一般處於UI線程,為了避免阻塞UI,我們必須把資料查詢等費時的工作放到單獨的Job中執行,一旦資料查詢完畢,我們又必須更新介面,這時我們又需要使用UI線程進行處理。下面是處理這種情況的範例程式碼:

button.addSelectionListener(new SelectionListener(){public void widgetSelected(SelectionEvent e){perform();}public void widgetDefaultSelected(SelectionEvent e){perform();}private void perform(){Job job = new Job(“擷取資料”){protected IStatus run(IProgressMonitor monitor){// 在此添加擷取資料的代碼Display.getDefault().asyncExec(new Runnable(){public void run(){// 在此添加更新介面的代碼}});}};job.schedule();}});

  • 延時執行Job,避免無用的Job運行

我們經常需要根據選中的對象重新整理我們部分的介面元素。如果我們連續很快的改變選擇,而每次重新整理介面涉及到的地區比較大時,介面會出現閃爍。從使用者的角度來說,我們很快的改變選擇,希望看到的只是最後選中的結果,中間的介面重新整理都是不必要的。

在Jface中,StructuredViewer提供了addPostSelectionChangedListener方法。如果我們使用這個方法監聽selectionChanged事件,當使用者一直按著方向鍵改變選中時,我們只會收到一個selectionChanged事件。這樣我們可以避免過度的重新整理介面。

實際上,Jface中就是通過延時執行Job來實現這一功能的。我們也可以自己實作類別似功能:

private final static Object UPDATE_UI_JOBFAMILY = new Object();tableviewer. addSelectionChangedListener (new ISelectionChangedListener (){public void selectionChanged(SelectionChangedEvent event){Job.getJobManager().cancel(UPDATE_UI_JOBFAMILY);new UIJob("更新介面") {            protected IStatus runInUIThread (IProgressMonitor monitor) {                //更新介面                return Status.OK_STATUS;             }public boolean belongsTo(Object family){return family== UPDATE_UI_JOBFAMILY;}        }.schedule(500);}});

首先,我們需要將介面更新的代碼放到一個UIJob中,同時我們將Job延時500毫秒執行(我們可以根據需要改變延時的時間)。如果下一個selectionChanged事件很快到來,我們的調用Job.getJobManager().cancel(UPDATE_UI_JOBFAMILY)將以前未啟動並執行Job取消,這樣只有最後一個Job會真正運行。

  • 在UI線程中等待非UI線程的結束

有時,我們在UI線程中需要等待一個非UI線程執行完,我們才能繼續執行。例如,我們在UI線程中要顯示某些資料,但是這些資料又需要從資料庫或者遠程網路擷取。於是,我們會啟動一個非UI的線程去擷取資料。而我們的UI線程必須要等待這個非UI線程執行完成,我們才能繼續執行。當然,一種簡單的實現方法是使用join。我們可以在UI線程中調用非UI線程的join方法,這樣我們就可以等待它執行完了,我們再繼續。但是,這會有一個問題。當我們的UI線程等待時,意味著我們的程式不會再響應介面操作,也不會重新整理。這樣,使用者會覺得我們的程式象死了一樣沒有反應。這時,我們可以使用ModalContext類。你可以將你要執行的擷取資料的任務用ModalContext的run方法來運行(如下)。ModalContext會將你的任務放到一個獨立的非UI線程中執行,並且等待它執行完再繼續執行。與join方法不同的是,ModalContext在等待時不會停止UI事件的處理。這樣我們的程式就不會沒有響應了。

try {   ModalContext.run(new IRunnableWithProgress(){    public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {     /*需要在非UI線程中執行的代碼*/     ModalContext.checkCanceled(monitor);    }       }, true, new NullProgressMonitor(), Display.getCurrent());  } catch (InvocationTargetException e) {     } catch (InterruptedException e) {     }

  • 針對相關聯的Job統一進行處理

有時,我們需要對相關聯的Job一起處理。例如需要同時取消這些Job,或者等待所有這些Job結束。這時我們可以使用Job Family。對於相關聯的Job,我們可以將它們設定成同一個Job Family。我們需要重載Job的belongsTo方法以設定一個Job的Job Family。

Private Object MY_JOB_FAMILY = new Object();Job job = new Job(“Job Name”){protected IStatus run(IProgressMonitor monitor) {// 在這裡添加你的任務代碼return Status.OK_STATUS;}public boolean belongsTo(Object family){return MY_JOB_FAMILY.equals(family);}};

我們可以使用JobManager的一系列方法針對Job Family進行操作:

Job.getJobManager().cancel(MY_JOB_FAMILY); //取消所有屬於MY_JOB_FAMILY的所有JobJob.getJobManager().join(MY_JOB_FAMILY); //等待屬於MY_JOB_FAMILY的所有Job結束Job.getJobManager().sleep(MY_JOB_FAMILY); //將所有屬於MY_JOB_FAMILY的Job轉入睡眠狀態Job.getJobManager().wakeup(MY_JOB_FAMILY); //將所有屬於MY_JOB_FAMILY的Job喚醒

線程死結的調試和解決技巧

一旦我們使用了線程,我們的程式中就有可能有死結的發生。一旦發生死結,我們發生死結的線程會沒有響應,導致我們程式效能下降。如果我們的UI線程發生了死結,我們的程式會沒有響應,必須要重啟程式。所以在我們多線程程式開發中,發現死結的情況,解決死結問題對提高我們程式的穩定性和效能極為重要。

如果我們發現程式運行異常(例如程式沒有響應),我們首先要確定是否發生了死結。通過下面這些步驟,我們可以確定是否死結以及死結的線程:

  • 在Eclipse中以Debug模式運行程式
  • 執行響應的測試案例重現問題
  • 在Eclipse的Debug View中選中主線程(Thread[main]),選擇菜單Run->Suspend。這時Eclipse會展開主線程的函數調用棧,我們就可以看到當前主線程正在執行的操作。
  • 通常,Eclipse在等待使用者的操作,它的函數調用棧會和以下類似:

    圖片樣本

  • 如果主線程發生死結,函數調用棧的最上層一般會是你自己的函數調用,你可以查看一下你當前的函數調用以確定主線程在等待什麼
  • 使用同樣的方法查看其他線程,特別是那些等待UI線程的線程

我們需要找出當前線程相互的等待關係,以便找出死結的原因。我們找出死結的線程後就可以針對不同情況進行處理:

  • 減小鎖的粒度,增加並發性
  • 調整資源請求的次序
  • 將需要等待資源的任務放到獨立的線程中執行

Job使用中要注意的問題

  • 不要在Job中使用Thread.sleep方法。如果你想要讓Job進入睡眠狀態,最好用Job的sleep方法。雖然,使用Thread.sleep和Job的sleep方法達到的效果差不多,但是它們實現的方式完全不同,對系統的影響也不一樣。我們知道Eclipse中Job是由Eclipse的JobManager來管理的。如果我們調用Job的sleep方法,JobManager會將Job轉入睡眠狀態,與其對應的線程也會重新放入線程池等待運行其他Job。而如果我們在Job中直接調用Thread.sleep方法,它會直接使運行Job的線程進入睡眠狀態,其他Job就不可能重用這個線程了。同時,雖然運行該Job的線程進入了睡眠狀態,Job的狀態還是Running(運行狀態),我們也不能用Job的wakeup方法喚醒該Job了
  • Job的取消。一般我們會直觀的認為,一旦調用Job的cancel方法,Job就會停止運行。實際上,這並不一定正確,當Job處於不同的狀態時,我們調用Job的cancel方法所起的效果是不同的。當Job在WAITING狀態和SLEEPING狀態時,一旦我們調用cancel方法,JobManager會將Job直接從等待啟動並執行隊列中刪除,Job不會再運行了,這時cancel方法會返回true。但是如果Job正在運行,cancel方法調用並不會立即終止Job的運行,它只會設定一個標誌,指明這個Job已經被取消了。我們可以使用Job的run方法傳入的參數IProgressMonitor monitor,這個參數的isCanceled方法會返回Job是否被取消的狀態。如果需要,我們必須在我們的代碼的適當位置檢查Job是否被取消的標誌,作出適當的響應。另外,由於調用Job的cancel方法不一定立即終止Job,如果我們需要等待被取消的Job運行完再執行,我們可以用如下代碼:
     if (!job.cancel())job.join();

  • Join方法的使用。由於join方法會導致一個線程等待另一個線程,一旦等待線程中擁有一個被等待線程所需要的鎖,就會產生死結。當我們的線程中需要用到同步時,這種死結的情況非常容易出現,所以我們使用join時必須非常小心,盡量以其他方法替代。
  • 避免過時的Job造成的錯誤。由於我們啟動的線程並不一定是馬上執行的,當我們的Job開始運行時,情況可能發生了變化。我們在Job的處理代碼中要考慮到這些情況。一種典型的情況是,我們在啟動一個對話方塊或者初始化一個ViewPart時,我們會啟動一些 Job去完成一些資料讀取的工作,一旦資料讀取結束,我們會啟動新的UI Job更新相應的UI。有時,使用者在開啟對話方塊或者View後,馬上關閉了該對話方塊或者View。這時我們啟動的線程並沒有被中斷,一旦在Job中再去更新UI,就會出錯。在我們的代碼中必須作相應的處理。所以,我們線上程中更新介面元素之前,我們必須先檢查相應的控制項是否已經被dispose了

結束語

在我們進行基於Eclipse的用戶端開發時,使用多線程可以大大的提供我們的程式並發處理能力,同時對於提高使用者體驗也有很好的協助。但是,多線程程式也有其不利的一面,我們也不要濫用線程:

  • 首先,多線程程式會大大的提高我們程式的複雜度,使得我們的開發和調試更加困難
  • 其次,過多的線程容易引發死結、資料同步等並發問題的發生
  • 另外,由於線程建立和銷毀需要開銷,程式的整體效能可能因為過多線程的使用而下降

所以,我們在使用線程時一定要謹慎。本文對Eclipse線程的討論,希望能對大家使用線程有所協助。由於實際情況較為複雜,文中所提到的方法僅供參考,讀者對於不同的實際問題需要進行具體分析,從而找出最佳的解決方案。

參考資料

  • 查看        developerWorks部落格的最新資訊。

關於作者

梁騫, IBM 中國軟體開發中心,現在從事 Workplace Managed Client 軟體的開發工作,通過 liangq@cn.ibm.com 可以與他聯絡。

 

轉載自:http://www.ibm.com/developerworks/cn/opensource/os-cn-eclipse-multithrd/

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.