Executors: How to synchronously wait until all tasks have finished if tasks are created recursively?

前端 未结 9 1427
傲寒
傲寒 2020-12-31 09:37

My question is strongly related to this one here. As was posted there, I would like the main thread to wait until the work queue is empty and all tasks have finished. The pr

相关标签:
9条回答
  • 2020-12-31 09:49

    Why don't you use a counter? For example:

    private AtomicInteger counter = new AtomicInteger(0);
    

    and increment the counter by one just before submitting the task to the queue:

    counter.incrementAndGet();
    

    and decrement it by one at the end of the task:

    counter.decrementAndGet();
    

    and the check would be something like:

    // ...
    while (counter.get() > 0);
    
    0 讨论(0)
  • 2020-12-31 09:55

    Java 7 provides a synchronizer that fits this use case called Phaser. It's a re-usable hybrid of a CountDownLatch and CyclicBarrier that can both increase and decrease the number of registered parties (similar to an incrementable CountDownLatch).

    The basic pattern to using the phaser in this scenario is to register tasks with the phaser when created and arrive when completed. When the number of arrived parties matches the number of registered, the phaser "advances" to the next phase, notifying any waiting threads of the advance when it takes place.

    Here's an example I've created of waiting for recursive task completion. It naively finds the first few numbers of the Fibonacci sequence for demonstration purposes:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ForkJoinPool;
    import java.util.concurrent.Phaser;
    import java.util.concurrent.atomic.AtomicLong;
    
    /**
     * An example of using a Phaser to wait for the completion of recursive tasks.
     * @author Voxelot
     */
    public class PhaserExample {
        /** Workstealing threadpool with reduced queue contention. */
        private static ForkJoinPool executors;
    
        /**
         * @param args the command line arguments
         */
        public static void main(String[] args) throws InterruptedException {
            executors = new ForkJoinPool();
            List<Long> sequence = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                sequence.add(fib(i));
            }
            System.out.println(sequence);
        }
    
        /**
         * Computes the nth Fibonacci number in the Fibonacci sequence.
         * @param n The index of the Fibonacci number to compute
         * @return The computed Fibonacci number
         */
        private static Long fib(int n) throws InterruptedException {
            AtomicLong result = new AtomicLong();
            //Flexible sychronization barrier
            Phaser phaser = new Phaser();
            //Base task
            Task initialTask = new Task(n, result, phaser);
            //Register fib(n) calling thread
            phaser.register();
            //Submit base task
            executors.submit(initialTask);
            //Make the calling thread arrive at the synchronization
            //barrier and wait for all future tasks to arrive.
            phaser.arriveAndAwaitAdvance();
            //Get the result of the parallel computation.
            return result.get();
        }
    
        private static class Task implements Runnable {
            /** The Fibonacci sequence index of this task. */
            private final int index;
            /** The shared result of the computation. */
            private final AtomicLong result;
            /** The synchronizer. */
            private final Phaser phaser;
    
            public Task(int n, AtomicLong result, Phaser phaser) {
                index = n;
                this.result = result;
                this.phaser = phaser;
                //Inform synchronizer of additional work to complete.
                phaser.register();
            }
    
            @Override
            public void run() {
                if (index == 1) {
                    result.incrementAndGet();
                } else if (index > 1) {
                    //recurrence relation: Fn = Fn-1 + Fn-2
                    Task task1 = new Task(index - 1, result, phaser);
                    Task task2 = new Task(index - 2, result, phaser);
                    executors.submit(task1);
                    executors.submit(task2);
                }
                //Notify synchronizer of task completion.
                phaser.arrive();
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-31 09:56

    Since the last task doesn't know that it's the last, I actually don't think it's possible to have this work 100% correctly without recording both when tasks launch and when they complete.

    If memory serves me right, the getQueue() method returns a queue containing only tasks that are still waiting to be executed, not ones that are currently running. Furthermore, getCompletedTaskCount() is approximate.

    The solution I'm pondering goes something like this, using an atomic counter like in Eng.Fouad's answer and a Condition for signaling the main thread to wake up (pardon the shortcuts for simplicity):

    public class MyThreadPoolExecutorState {
    
        public final Lock lock = new ReentrantLock();
        public final Condition workDone = lock.newCondition();
        public boolean workIsDone = false;
    
    }
    
    public class MyThreadPoolExecutor extends ThreadPoolExecutor {
    
        private final MyThreadPoolExecutorState state;
        private final AtomicInteger counter = new AtomicInteger(0);
    
        public MyThreadPoolExecutor(MyThreadPoolExecutorState state, ...) {
            super(...);
            this.state = state;
        }
    
        protected void beforeExecute(Thread t, Runnable r) {
            this.counter.incrementAndGet();
        }
    
        protected void afterExecute(Runnable r, Throwable t) {
            if(this.counter.decrementAndGet() == 0) {
                this.state.lock.lock();
                try {
                    this.state.workIsDone = true;
                    this.state.workDone.signal();
                }
                finally {
                    this.state.lock.unlock();
                }
            }
        }
    
    }
    
    public class MyApp {
    
        public static void main(...) {
    
            MyThreadPoolExecutorState state = new MyThreadPoolExecutorState();
            MyThreadPoolExecutor executor = new MyThreadPoolExecutor(state, ...);
    
            // Fire ze missiles!
            executor.submit(...);
    
            state.lock.lock();
            try {
                while(state.workIsDone == false) {
                    state.workDone.await();
                }
            }
            finally {
                state.lock.unlock();
            }
    
        }
    
    }
    

    It could be a little more elegant (maybe just provide a getState() in your thread pool executor or something?), but I think it should get the job done. It's also untested, so implement at your own peril...

    It is worth noting that this solution will definitely fail if there are no tasks to be executed -- it'll await the signal indefinitely. So don't even bother starting the executor if you have no tasks to run.


    Edit: On second thought, incrementing the atomic counter should happen upon submission, not immediately before task execution (because queuing could cause the counter to fall to 0 prematurely). It probably makes sense to override the submit(...) methods instead, and possibly also remove(...) and shutdown() (if you use them). The general idea remains the same, though. (But the more I think about it, the less pretty it is.)

    I'd also check out the internals of the class to see if you can glean any knowledge from it: http://hg.openjdk.java.net/build-infra/jdk7/jdk/file/0f8da27a3ea3/src/share/classes/java/util/concurrent/ThreadPoolExecutor.java. The tryTerminate() method looks interesting.

    0 讨论(0)
  • 2020-12-31 09:56

    You could use an atomic counter to count the submit (like has been said, before actually submitting). Combine this with a semaphore and release it in the afterExecute hook that a ThreadPoolExecutor provides. Instead of busy-waiting, call semaphore.acquire( counter.get()) after the first round of jobs has been submitted. But the number of acquires will be too small when calling acquire since the counter may increase later on. You would have to loop the acquire calls, with the increase since the last call as the argument, until the counter does not increase anymore.

    0 讨论(0)
  • 2020-12-31 10:04

    Java 7 has incorporated support for recursive tasks via its ForkJoinPool executor. It is quite simple to use and scales quite well, as long as the tasks themselves are not too trivial. Essentially it provides a controlled interface that allows tasks to wait for the completion of any sub-tasks without blocking the underlying thread indefinitely.

    0 讨论(0)
  • 2020-12-31 10:07

    If you know number of threads to wait and can paste one line of code to increase number for each thread with help of CountDownLatch ( http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/CountDownLatch.html ) It can resolve you problem

    0 讨论(0)
提交回复
热议问题