![](/img/trans.png)
[英]Java ExecutorService - What if awaitTermination() fails?
[英]Java ExecutorService: awaitTermination of all recursively created tasks
我使用ExecutorService
執行任務。 該任務可以遞歸創建其他任務,這些其他任務提交給相同的ExecutorService
而那些子任務也可以這樣做。
我現在遇到的問題是,我要等到所有任務都完成(即所有任務都已完成並且它們沒有提交新任務)后再繼續。
我無法在主線程中調用ExecutorService.shutdown()
,因為這會阻止ExecutorService
接受新任務。
如果尚未調用shutdown
則調用ExecutorService.awaitTermination()
似乎無濟於事。
所以我有點卡在這里。 對於ExecutorService
,看到所有工作人員都空閑並不難,不是嗎? 我能想到的唯一優雅的解決方案是直接使用ThreadPoolExecutor
並每隔一段時間查詢其getPoolSize()
。 真的沒有更好的方法嗎?
如果最初不清楚遞歸任務樹中的任務數量,則最簡單的方法可能是實現自己的同步原語(某種“反信號量”),並在任務之間共享它。 在提交每個任務之前,您需要增加一個值,當任務完成時,它會減少該值,然后等待直到該值為0。
將其實現為從任務中顯式調用的單獨原語,可以將此邏輯與線程池實現分離開來,並允許您將多個獨立的遞歸任務樹提交到同一池中。
像這樣:
public class InverseSemaphore {
private int value = 0;
private Object lock = new Object();
public void beforeSubmit() {
synchronized(lock) {
value++;
}
}
public void taskCompleted() {
synchronized(lock) {
value--;
if (value == 0) lock.notifyAll();
}
}
public void awaitCompletion() throws InterruptedException {
synchronized(lock) {
while (value > 0) lock.wait();
}
}
}
注意, taskCompleted()
應該在finally
塊中調用,以使其不受可能的異常影響。
還要注意,應在提交任務之前由提交線程調用beforeSubmit()
,而不是由任務本身調用,以避免在舊任務完成而新任務尚未開始時可能出現的“錯誤完成”。
編輯:使用模式已修復的重要問題。
這確實是Phaser的理想候選人。 Java 7即將推出這一新類。 它是靈活的CountdonwLatch / CyclicBarrier。 您可以在JSR 166 Interest Site獲得穩定版本。
CountdownLatch / CyclicBarrier更加靈活的方式是因為它不僅能夠支持未知數目的參與者(線程),而且還可以重用(這就是階段部分所在的位置)
對於您提交的每個任務,您都將進行注冊,當該任務完成時,您便會到達。 這可以遞歸完成。
Phaser phaser = new Phaser();
ExecutorService e = //
Runnable recursiveRunnable = new Runnable(){
public void run(){
//do work recursively if you have to
if(shouldBeRecursive){
phaser.register();
e.submit(recursiveRunnable);
}
phaser.arrive();
}
}
public void doWork(){
int phase = phaser.getPhase();
phaser.register();
e.submit(recursiveRunnable);
phaser.awaitAdvance(phase);
}
編輯:感謝@depthofreality指出我的上一個示例中的比賽條件。 我正在對其進行更新,以便正在執行的線程僅等待當前階段的前進,因為它阻塞了遞歸函數的完成。
相數直到arrive
數s == register
s才會跳閘。 由於在每個遞歸調用之前都進行register
所以當所有調用完成時,將發生相位遞增。
哇,你們很快:)
感謝您的所有建議。 期貨不容易與我的模型集成,因為我不知道事先預定了多少可運行對象。 因此,如果我讓父任務活着只是為了等待它的遞歸子任務完成,那么我周圍就會有很多垃圾。
我使用AtomicInteger建議解決了我的問題。 本質上,我將ThreadPoolExecutor子類化,並在對execute()的調用中增加計數器,在對afterExecute()的調用中減少計數器。 當計數器為0時,我調用shutdown()。 這似乎可以解決我的問題,不確定這樣做是否是通常的好方法。 特別是,我假設您僅使用execute()添加Runnable。
作為副節點:我首先嘗試檢入afterExecute()隊列中的Runnable數量,以及當它們為0時活動和關閉的工作程序的數量; 但這不起作用,因為並非所有Runnable都出現在隊列中,並且getActiveCount()也不符合我的預期。
無論如何,這是我的解決方案:(如果有人發現嚴重問題,請告訴我:)
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
private final AtomicInteger executing = new AtomicInteger(0);
public MyThreadPoolExecutor(int coorPoolSize, int maxPoolSize, long keepAliveTime,
TimeUnit seconds, BlockingQueue<Runnable> queue) {
super(coorPoolSize, maxPoolSize, keepAliveTime, seconds, queue);
}
@Override
public void execute(Runnable command) {
//intercepting beforeExecute is too late!
//execute() is called in the parent thread before it terminates
executing.incrementAndGet();
super.execute(command);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
int count = executing.decrementAndGet();
if(count == 0) {
this.shutdown();
}
}
}
您可以創建自己的線程池,以擴展ThreadPoolExecutor 。 您想知道任務何時提交以及任務何時完成。
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
private int counter = 0;
public MyThreadPoolExecutor() {
super(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
}
@Override
public synchronized void execute(Runnable command) {
counter++;
super.execute(command);
}
@Override
protected synchronized void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
counter--;
notifyAll();
}
public synchronized void waitForExecuted() throws InterruptedException {
while (counter == 0)
wait();
}
}
對您的任務使用Future (而不是提交Runnable
),回調將在完成時更新其狀態,因此您可以使用Future.isDone來跟蹤所有任務的狀態。
我必須說,上述遞歸調用任務和等待結束子任務的問題的解決方案令我不滿意。 我的解決方案受到Oracle原始文檔的啟發: CountDownLatch和那里的示例: 人力資源CountDownLatch 。
類HRManagerCompact實例中正在處理的第一個公共線程具有等待閂鎖的兩個子線程,而擁有等待閂鎖的后續的兩個子線程...等等。
當然,閂鎖可以設置為不同於2的值(在CountDownLatch的構造函數中),並且可以在迭代中建立可運行對象的數量(即ArrayList),但是它必須是對應的(遞減計數必須等於參數在CountDownLatch構造函數中)。
注意,鎖存器的數量會根據限制條件'level.get()<2'以及對象的數量呈指數增長。 1、2、4、8、16 ...和鎖存器0、1、2、4 ...如您所見,對於四個級別(level.get()<4),將有15個等待線程和7個鎖存器在當時,高峰16個線程正在運行。
package processes.countdownlatch.hr;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/** Recursively latching running classes to wait for the peak threads
*
* @author hariprasad
*/
public class HRManagerCompact extends Thread {
final int N = 2; // number of daughter's tasks for latch
CountDownLatch countDownLatch;
CountDownLatch originCountDownLatch;
AtomicInteger level = new AtomicInteger(0);
AtomicLong order = new AtomicLong(0); // id latched thread waiting for
HRManagerCompact techLead1 = null;
HRManagerCompact techLead2 = null;
HRManagerCompact techLead3 = null;
// constructor
public HRManagerCompact(CountDownLatch countDownLatch, String name,
AtomicInteger level, AtomicLong order){
super(name);
this.originCountDownLatch=countDownLatch;
this.level = level;
this.order = order;
}
private void doIt() {
countDownLatch = new CountDownLatch(N);
AtomicInteger leveli = new AtomicInteger(level.get() + 1);
AtomicLong orderi = new AtomicLong(Thread.currentThread().getId());
techLead1 = new HRManagerCompact(countDownLatch, "first", leveli, orderi);
techLead2 = new HRManagerCompact(countDownLatch, "second", leveli, orderi);
//techLead3 = new HRManagerCompact(countDownLatch, "third", leveli);
techLead1.start();
techLead2.start();
//techLead3.start();
try {
synchronized (Thread.currentThread()) { // to prevent print and latch in the same thread
System.out.println("*** HR Manager waiting for recruitment to complete... " + level + ", " + order + ", " + orderi);
countDownLatch.await(); // wait actual thread
}
System.out.println("*** Distribute Offer Letter, it means finished. " + level + ", " + order + ", " + orderi);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ": working... " + level + ", " + order + ", " + Thread.currentThread().getId());
Thread.sleep(10*level.intValue());
if (level.get() < 2) doIt();
Thread.yield();
}
catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/*catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
// TODO Auto-generated method stub
System.out.println("--- " +Thread.currentThread().getName() + ": recruted " + level + ", " + order + ", " + Thread.currentThread().getId());
originCountDownLatch.countDown(); // count down
}
public static void main(String args[]){
AtomicInteger levelzero = new AtomicInteger(0);
HRManagerCompact hr = new HRManagerCompact(null, "zero", levelzero, new AtomicLong(levelzero.longValue()));
hr.doIt();
}
}
可能的評論輸出(有可能):
first: working... 1, 1, 10 // thread 1, first daughter's task (10)
second: working... 1, 1, 11 // thread 1, second daughter's task (11)
first: working... 2, 10, 12 // thread 10, first daughter's task (12)
first: working... 2, 11, 14 // thread 11, first daughter's task (14)
second: working... 2, 11, 15 // thread 11, second daughter's task (15)
second: working... 2, 10, 13 // thread 10, second daughter's task (13)
--- first: recruted 2, 10, 12 // finished 12
--- first: recruted 2, 11, 14 // finished 14
--- second: recruted 2, 10, 13 // finished 13 (now can be opened latch 10)
--- second: recruted 2, 11, 15 // finished 15 (now can be opened latch 11)
*** HR Manager waiting for recruitment to complete... 0, 0, 1
*** HR Manager waiting for recruitment to complete... 1, 1, 10
*** Distribute Offer Letter, it means finished. 1, 1, 10 // latch on 10 opened
--- first: recruted 1, 1, 10 // finished 10
*** HR Manager waiting for recruitment to complete... 1, 1, 11
*** Distribute Offer Letter, it means finished. 1, 1, 11 // latch on 11 opened
--- second: recruted 1, 1, 11 // finished 11 (now can be opened latch 1)
*** Distribute Offer Letter, it means finished. 0, 0, 1 // latch on 1 opened
我能想到的唯一優雅的解決方案是直接使用ThreadPoolExecutor並每隔一段時間查詢其getPoolSize()。 真的沒有更好的方法嗎?
您必須按正確的順序使用shutdown() ,
awaitTermination() and shutdownNow()
方法。
shutdown()
:啟動有序關閉,在該關閉中執行先前提交的任務,但不接受任何新任務。
awaitTermination()
:阻塞直到關閉請求后所有任務完成執行,或者發生超時,或者當前線程被中斷(以先發生者為准)。
shutdownNow()
:嘗試停止所有正在執行的任務,暫停正在等待的任務的處理,並返回正在等待執行的任務的列表。
從ExecutorService的 oracle文檔頁面推薦的方法:
void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
System.err.println("Pool did not terminate");
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
如果長時間完成任務,則可以將if條件替換為while條件,如下所示:
更改
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
至
while(!pool.awaitTermination(60, TimeUnit.SECONDS)) {
Thread.sleep(60000);
}
您可以在以下內容中引用其他替代方法( join()
除外,該方法可以與獨立線程一起使用):
使用CountDownLatch 。 將CountDownLatch對象傳遞給每個任務,並對任務進行編碼,如下所示。
public void doTask() {
// do your task
latch.countDown();
}
而需要等待的線程應執行以下代碼:
public void doWait() {
latch.await();
}
但是,當然,這假設您已經知道子任務的數量,以便可以初始化閂鎖的計數。
您可以使用運行程序來跟蹤正在運行的線程:
Runner runner = Runner.runner(numberOfThreads);
runner.runIn(2, SECONDS, callable);
runner.run(callable);
// blocks until all tasks are finished (or failed)
runner.waitTillDone();
// and reuse it
runner.runRunnableIn(500, MILLISECONDS, runnable);
runner.waitTillDone();
// and then just kill it
runner.shutdownAndAwaitTermination();
要使用它,您只需添加一個依賴項:
編譯'com.github.matejtymes:javafixes:1.3.0'
(是罪魁禍首:這是我睡前的“一點點”;)但這是動態鎖存的首次嘗試):
package oss.alphazero.sto4958330;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class DynamicCountDownLatch {
@SuppressWarnings("serial")
private static final class Sync extends AbstractQueuedSynchronizer {
private final CountDownLatch toplatch;
public Sync() {
setState(0);
this.toplatch = new CountDownLatch(1);
}
@Override
protected int tryAcquireShared(int acquires){
try {
toplatch.await();
}
catch (InterruptedException e) {
throw new RuntimeException("Interrupted", e);
}
return getState() == 0 ? 1 : -1;
}
public boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
public boolean tryExtendState(int acquires) {
for (;;) {
int s = getState();
int exts = s+1;
if (compareAndSetState(s, exts)) {
toplatch.countDown();
return exts > 0;
}
}
}
}
private final Sync sync;
public DynamicCountDownLatch(){
this.sync = new Sync();
}
public void await()
throws InterruptedException
{
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException
{
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public void join() {
sync.tryExtendState(1);
}
}
此閂鎖將新方法join()引入到現有的(克隆的)CountDownLatch API中,任務使用該方法來表示它們已進入較大的任務組。
閂鎖從父任務傳遞到子任務。 按照Suraj的模式,每個任務將首先“閂鎖” join(),執行其task(),然后執行countDown()。
為了解決主線程啟動任務組然后立即進行awaits()的情況-在任何任務線程甚至沒有機會加入join()之前,將topLatch
用於內部Sync
類。 這是一個鎖存器,將在每個join()上計數。 當然,只有第一個倒計時很重要,因為所有后續倒數都是點數。
上面的初始實現確實引入了某種語義上的折皺,因為tryAcquiredShared(int)不應引發InterruptedException,但是我們確實需要在topLatch的等待中處理中斷。
這是對OP自己使用原子計數器的解決方案的改進嗎? 我想說的可能不是IFF,他堅持使用Executor,但我認為,在那種情況下,它是使用AQS的同等有效的替代方法,並且也可用於通用線程。
甩掉其他黑客。
如果您想使用JSR166y類(例如Phaser或Fork / Join),它們中的任何一個都可能對您有用,則可以始終從以下網站下載它們的Java 6反向端口: http : //gee.cs.oswego.edu/dl/concurrency -interest /,並以此為基礎,而不是編寫完全自制的解決方案。 然后,當7出現時,您可以只刪除對backport的依賴關系並更改一些軟件包名稱。
(完整披露:我們已經在產品中使用LinkedTransferQueue已有一段時間了。沒有問題)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.