Исполнители: как синхронно ждать завершения всех задач, если задачи создаются рекурсивно?

Мой вопрос сильно связан с этим здесь. Как было опубликовано там, я хотел бы, чтобы основной поток подождал, пока рабочая очередь не будет пустой, и все задачи будут завершены. Однако проблема в моей ситуации заключается в том, что каждая задача может рекурсивно вызывать новые задачи для обработки. Это затрудняет сбор всех этих задач фьючерсами.

Наше текущее решение использует цикл "занято-ожидание" для ожидания завершения:

        do { //Wait until we are done the processing
      try {
        Thread.sleep(200);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    } while (!executor.getQueue().isEmpty()
             || numTasks.longValue() > executor.getCompletedTaskCount());

numTasks - это значение, которое увеличивается по мере создания каждой новой задачи. Это работает, но я думаю, что это не очень приятно из-за оживленного ожидания. Мне было интересно, есть ли хороший способ заставить основной поток ждать синхронно, пока явно не проснется.

Ответ 1

Большое спасибо за все ваши предложения!

В конце концов я выбрал то, что, по моему мнению, достаточно прост. Я узнал, что CountDownLatch почти что мне нужно. Он блокируется до тех пор, пока счетчик не достигнет 0. Единственная проблема заключается в том, что он может только отсчет, а не вверх, и, следовательно, не работает в динамическом параметре, где задачи могут отправлять новые задачи. Поэтому я реализовал новый класс CountLatch, который предлагает дополнительную функциональность. (см. ниже) Этот класс я используется следующим образом.

Вызов основного потока latch.awaitZero(), блокировка до тех пор, пока защелка не достигнет 0.

Любой поток, перед вызовом executor.execute(..) вызывает latch.increment().

Любая задача, незадолго до завершения, вызывает latch.decrement().

Когда последняя задача завершается, счетчик достигнет 0 и, таким образом, освободит основной поток.

Дальнейшие предложения и отзывы приветствуются!

public class CountLatch {

@SuppressWarnings("serial")
private static final class Sync extends AbstractQueuedSynchronizer {

    Sync(int count) {
        setState(count);
    }

    int getCount() {
        return getState();
    }

    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    protected int acquireNonBlocking(int acquires) {
        // increment count
        for (;;) {
            int c = getState();
            int nextc = c + 1;
            if (compareAndSetState(c, nextc))
                return 1;
        }
    }

    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

private final Sync sync;

public CountLatch(int count) {
    this.sync = new Sync(count);
}

public void awaitZero() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public boolean awaitZero(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public void increment() {
    sync.acquireNonBlocking(1);
}

public void decrement() {
    sync.releaseShared(1);
}

public String toString() {
    return super.toString() + "[Count = " + sync.getCount() + "]";
}

}

Обратите внимание, что вызовы increment()/decrement() могут быть инкапсулированы в настраиваемый подкласс Executor, как было предложено, например, Сами Корхоненом или с помощью beforeExecute и afterExecute, как было предложено им. См. Здесь:

public class CountingThreadPoolExecutor extends ThreadPoolExecutor {

protected final CountLatch numRunningTasks = new CountLatch(0);

public CountingThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
        BlockingQueue<Runnable> workQueue) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}

@Override
public void execute(Runnable command) {
    numRunningTasks.increment();
    super.execute(command);
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
    numRunningTasks.decrement();
    super.afterExecute(r, t);
}

/**
 * Awaits the completion of all spawned tasks.
 */
public void awaitCompletion() throws InterruptedException {
    numRunningTasks.awaitZero();
}

/**
 * Awaits the completion of all spawned tasks.
 */
public void awaitCompletion(long timeout, TimeUnit unit) throws InterruptedException {
    numRunningTasks.awaitZero(timeout, unit);
}

}

Ответ 2

Это была довольно интересная проблема для решения. Я должен предупредить, что я полностью не тестировал код.

Идея состоит в том, чтобы просто отслеживать выполнение задачи:

  • Если задача успешно поставлена ​​в очередь, счетчик увеличивается на один
  • если задача отменена и она не была выполнена, счетчик уменьшается на один
  • Если задача выполнена, счетчик уменьшается на

Когда вызывается выключение и выполняются ожидающие задачи, делегат не будет вызывать завершение работы на фактическом ExecutorService. Это позволит запускать новые задачи, пока ожидающее количество задач не достигнет нуля, а выключение будет вызвано фактическим ExecutorService.

public class ResilientExecutorServiceDelegate implements ExecutorService {
    private final ExecutorService executorService;
    private final AtomicInteger pendingTasks;
    private final Lock readLock;
    private final Lock writeLock;
    private boolean isShutdown;

    public ResilientExecutorServiceDelegate(ExecutorService executorService) {
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        this.pendingTasks = new AtomicInteger();
        this.readLock = readWriteLock.readLock();
        this.writeLock = readWriteLock.writeLock();
        this.executorService = executorService;
        this.isShutdown = false;
    }

    private <T> T addTask(Callable<T> task) {
        T result;
        boolean success = false;
        // Increment pending tasks counter
        incrementPendingTaskCount();
        try {
            // Call service
            result = task.call();
            success = true;
        } catch (RuntimeException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new RejectedExecutionException(exception);
        } finally {
            if (!success) {
                // Decrement pending tasks counter
                decrementPendingTaskCount();
            }
        }
        return result;
    }

    private void incrementPendingTaskCount() {
        pendingTasks.incrementAndGet();
    }

    private void decrementPendingTaskCount() {
        readLock.lock();
        if (pendingTasks.decrementAndGet() == 0 && isShutdown) {
            try {
                // Shutdown
                executorService.shutdown();
            } catch (Throwable throwable) {
            }
        }
        readLock.unlock();
    }

    @Override
    public void execute(final Runnable task) {
        // Add task
        addTask(new Callable<Object>() {
            @Override
            public Object call() {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            task.run();
                        } finally {
                            decrementPendingTaskCount();
                        }
                    }
                });
                return null;
            }
        });
    }

    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
        // Call service
        return executorService.awaitTermination(timeout, unit);
    }

    @Override
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
            throws InterruptedException {
        // It ok to increment by just one
        incrementPendingTaskCount();
        try {
            return executorService.invokeAll(tasks);
        } finally {
            decrementPendingTaskCount();
        }
    }

    @Override
    public <T> List<Future<T>> invokeAll(
            Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
            throws InterruptedException {
        // It ok to increment by just one
        incrementPendingTaskCount();
        try {
            return executorService.invokeAll(tasks, timeout, unit);
        } finally {
            decrementPendingTaskCount();
        }
    }

    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
            throws InterruptedException, ExecutionException {
        // It ok to increment by just one
        incrementPendingTaskCount();
        try {
            return executorService.invokeAny(tasks);
        } finally {
            decrementPendingTaskCount();
        }
    }

    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
            long timeout, TimeUnit unit) throws InterruptedException,
            ExecutionException, TimeoutException {
        incrementPendingTaskCount();
        try {
            return executorService.invokeAny(tasks, timeout, unit);
        } finally {
            decrementPendingTaskCount();
        }
    }

    @Override
    public boolean isShutdown() {
        return isShutdown;
    }

    @Override
    public boolean isTerminated() {
        return executorService.isTerminated();
    }

    @Override
    public void shutdown() {
        // Lock write lock
        writeLock.lock();
        // Set as shutdown
        isShutdown = true;
        try {
            if (pendingTasks.get() == 0) {
                // Real shutdown
                executorService.shutdown();
            }
        } finally {
            // Unlock write lock
            writeLock.unlock();
        }
    }

    @Override
    public List<Runnable> shutdownNow() {
        // Lock write lock
        writeLock.lock();
        // Set as shutdown
        isShutdown = true;
        // Unlock write lock
        writeLock.unlock();

        return executorService.shutdownNow();
    }

    @Override
    public <T> Future<T> submit(final Callable<T> task) {
        // Create execution status
        final FutureExecutionStatus futureExecutionStatus = new FutureExecutionStatus();
        // Add task
        return addTask(new Callable<Future<T>>() {
            @Override
            public Future<T> call() {
                return new FutureDelegate<T>(
                        executorService.submit(new Callable<T>() {
                            @Override
                            public T call() throws Exception {
                                try {
                                    // Mark as executed
                                    futureExecutionStatus.setExecuted();
                                    // Run the actual task
                                    return task.call();
                                } finally {
                                    decrementPendingTaskCount();
                                }
                            }
                        }), futureExecutionStatus);
            }
        });
    }

    @Override
    public Future<?> submit(final Runnable task) {
        // Create execution status
        final FutureExecutionStatus futureExecutionStatus = new FutureExecutionStatus();
        // Add task
        return addTask(new Callable<Future<?>>() {
            @Override
            @SuppressWarnings("unchecked")
            public Future<?> call() {
                return new FutureDelegate<Object>(
                        (Future<Object>) executorService.submit(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    // Mark as executed
                                    futureExecutionStatus.setExecuted();
                                    // Run the actual task
                                    task.run();
                                } finally {
                                    decrementPendingTaskCount();
                                }
                            }
                        }), futureExecutionStatus);
            }
        });
    }

    @Override
    public <T> Future<T> submit(final Runnable task, final T result) {
        // Create execution status
        final FutureExecutionStatus futureExecutionStatus = new FutureExecutionStatus();
        // Add task
        return addTask(new Callable<Future<T>>() {
            @Override
            public Future<T> call() {
                return new FutureDelegate<T>(executorService.submit(
                        new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    // Mark as executed
                                    futureExecutionStatus.setExecuted();
                                    // Run the actual task
                                    task.run();
                                } finally {
                                    decrementPendingTaskCount();
                                }
                            }
                        }, result), futureExecutionStatus);
            }
        });
    }

    private class FutureExecutionStatus {
        private volatile boolean executed;

        public FutureExecutionStatus() {
            executed = false;
        }

        public void setExecuted() {
            executed = true;
        }

        public boolean isExecuted() {
            return executed;
        }
    }

    private class FutureDelegate<T> implements Future<T> {
        private Future<T> future;
        private FutureExecutionStatus executionStatus;

        public FutureDelegate(Future<T> future,
                FutureExecutionStatus executionStatus) {
            this.future = future;
            this.executionStatus = executionStatus;
        }

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            boolean cancelled = future.cancel(mayInterruptIfRunning);
            if (cancelled) {
                // Lock read lock
                readLock.lock();
                // If task was not executed
                if (!executionStatus.isExecuted()) {
                    decrementPendingTaskCount();
                }
                // Unlock read lock
                readLock.unlock();
            }
            return cancelled;
        }

        @Override
        public T get() throws InterruptedException, ExecutionException {
            return future.get();
        }

        @Override
        public T get(long timeout, TimeUnit unit) throws InterruptedException,
                ExecutionException, TimeoutException {
            return future.get(timeout, unit);
        }

        @Override
        public boolean isCancelled() {
            return future.isCancelled();
        }

        @Override
        public boolean isDone() {
            return future.isDone();
        }
    }
}

Ответ 3

Java 7 предоставляет синхронизатор, который подходит для этого случая использования Phaser. Это повторно используемый гибрид CountDownLatch и CyclicBarrier, который может увеличивать и уменьшать количество зарегистрированных сторон (подобно инкрементному CountDownLatch).

Основной шаблон использования фазера в этом сценарии - register задачи с фазером при создании и arrive по завершении. Когда количество прибывших партий совпадает с количеством зарегистрированных, фазер "продвигается" к следующему этапу, уведомляя любой ожидающий поток продвижения, когда он имеет место.

Вот пример, который я создал для ожидания завершения рекурсивной задачи. Он наивно находит первые несколько чисел последовательности Фибоначчи для демонстрационных целей:

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();
        }
    }
}

Ответ 4

Почему бы вам не использовать счетчик? Например:

private AtomicInteger counter = new AtomicInteger(0);

и увеличивайте счетчик на единицу перед отправкой задачи в очередь:

counter.incrementAndGet();

и уменьшите его на один в конце задачи:

counter.decrementAndGet();

и проверка будет выглядеть примерно так:

// ...
while (counter.get() > 0);

Ответ 5

В ответах, на которые вы ссылаетесь, предлагается использовать CompletionService

Вы можете заменить ожидание в своем основном потоке:

while (true) {
    Future<?> f = completionService.take(); //blocks until task completes
    if (executor.getQueue().isEmpty()
         && numTasks.longValue() == executor.getCompletedTaskCount()) break;
}

Обратите внимание, что getCompletedTaskCount возвращает только приблизительное число, поэтому вам может потребоваться найти лучшее условие выхода.

Ответ 6

Java 7 включила поддержку рекурсивных задач через свой ForkJoinPool исполнитель. достаточно прост в использовании и достаточно хорошо масштабируется, если сами задачи не слишком тривиальны. По сути, он обеспечивает контролируемый интерфейс, который позволяет задачам ждать завершения любых подзадач без блокировки основного потока неопределенно.

Ответ 7

Если вы знаете количество потоков для ожидания и можете вставить одну строку кода, чтобы увеличить число для каждого потока с помощью CountDownLatch ( http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/CountDownLatch.html) Он может решить вашу проблему.

Ответ 8

Поскольку последняя задача не знает, что она последняя, ​​я на самом деле не думаю, что можно правильно выполнить эту работу на 100% без записи как при запуске задач, так и при их завершении.

Если память мне подходит, метод getQueue() возвращает очередь, содержащую только задачи, которые все еще ждут выполнения, а не те, которые в настоящее время запущены. Кроме того, getCompletedTaskCount() является приблизительным.

Решение, которое я обдумываю, похоже на это, используя атомный счетчик, как в ответе Eng.Fouad, и Condition для сигнализации основной поток для пробуждения (помилование ярлыков для простоты):

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();
        }

    }

}

Это может быть немного более элегантным (может быть, просто предоставить getState() в исполнителе пула потоков или что-то в этом роде), но я думаю, что он должен выполнить эту работу. Он также непроверен, поэтому реализуйте свою собственную угрозу...

Стоит отметить, что это решение, безусловно, потерпит неудачу, если нет задач, которые нужно выполнить - он будет долго ждать сигнала. Поэтому даже не начинайте запускать исполнителя, если у вас нет задач для запуска.


Изменить: Во-вторых, приращение атомарного счетчика должно происходить при отправке, а не сразу перед выполнением задачи (потому что очередность может привести к тому, что счетчик упадет до 0 преждевременно). Возможно, имеет смысл переопределить методы submit(...) и, возможно, также remove(...) и shutdown() (если вы их используете). Однако общая идея остается прежней. (Но чем больше я думаю об этом, тем менее он хорош.)

Я также проверил внутренности класса, чтобы узнать, можете ли вы извлечь из него какие-либо знания: http://hg.openjdk.java.net/build-infra/jdk7/jdk/file/0f8da27a3ea3/src/share/classes/java/util/concurrent/ThreadPoolExecutor.java. Метод tryTerminate() выглядит интересным.

Ответ 9

Вы можете использовать атомный счетчик для подсчета отправки (как было сказано, перед фактической отправкой). Объедините это с помощью семафора и отпустите его в hook afterExecute, который предоставляет ThreadPoolExecutor. Вместо оживленного вызова вызовите semaphore.acquire( counter.get()) после того, как был отправлен первый раунд заданий. Но количество приобретений будет слишком маленьким при вызове приобретать, так как счетчик может увеличиться позже. Вам придется зацикливать вызовы на покупку с увеличением с момента последнего вызова в качестве аргумента, пока счетчик больше не увеличится.