簡體   English   中英

Java ExecutorService:所有遞歸創建的任務的awaitTermination

[英]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()除外,該方法可以與獨立線程一起使用):

等到所有線程在Java中完成工作

使用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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM