Исполнители Java: как получать уведомления, не блокируя, когда задача завершается?

154

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

  1. Взять задание из очереди
  2. Отправить его исполнителю
  3. Позвоните .get на возвращенное будущее и заблокируйте, пока результат не будет доступен
  4. Возьми еще одно задание из очереди ...

Тем не менее, я пытаюсь избежать блокировки полностью. Если у меня будет 10 000 таких очередей, для которых нужно обрабатывать свои задачи по одной, я исчерпаю пространство стека, потому что большинство из них будут удерживать заблокированные потоки.

То, что я хотел бы, это представить задачу и предоставить обратный вызов, который вызывается, когда задача завершена. Я буду использовать это уведомление об обратном вызове в качестве флага для отправки следующего задания. (Functional Java и Jetlang, очевидно, используют такие неблокирующие алгоритмы, но я не могу понять их код)

Как я могу сделать это, используя java.util.concurrent JDK, если не считать написания моей собственной службы исполнителя?

(очередь, которая кормит меня этими задачами, может сама блокироваться, но это проблема, которая будет решена позже)

Shahbaz
источник

Ответы:

146

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

Вы даже можете написать общую оболочку для задач Runnable и отправить их в ExecutorService. Или см. Ниже механизм, встроенный в Java 8.

class CallbackTask implements Runnable {

  private final Runnable task;

  private final Callback callback;

  CallbackTask(Runnable task, Callback callback) {
    this.task = task;
    this.callback = callback;
  }

  public void run() {
    task.run();
    callback.complete();
  }

}

В CompletableFutureJava 8 включены более сложные средства для составления конвейеров, в которых процессы могут выполняться асинхронно и условно. Вот надуманный, но полный пример уведомления.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class GetTaskNotificationWithoutBlocking {

  public static void main(String... argv) throws Exception {
    ExampleService svc = new ExampleService();
    GetTaskNotificationWithoutBlocking listener = new GetTaskNotificationWithoutBlocking();
    CompletableFuture<String> f = CompletableFuture.supplyAsync(svc::work);
    f.thenAccept(listener::notify);
    System.out.println("Exiting main()");
  }

  void notify(String msg) {
    System.out.println("Received message: " + msg);
  }

}

class ExampleService {

  String work() {
    sleep(7000, TimeUnit.MILLISECONDS); /* Pretend to be busy... */
    char[] str = new char[5];
    ThreadLocalRandom current = ThreadLocalRandom.current();
    for (int idx = 0; idx < str.length; ++idx)
      str[idx] = (char) ('A' + current.nextInt(26));
    String msg = new String(str);
    System.out.println("Generated message: " + msg);
    return msg;
  }

  public static void sleep(long average, TimeUnit unit) {
    String name = Thread.currentThread().getName();
    long timeout = Math.min(exponential(average), Math.multiplyExact(10, average));
    System.out.printf("%s sleeping %d %s...%n", name, timeout, unit);
    try {
      unit.sleep(timeout);
      System.out.println(name + " awoke.");
    } catch (InterruptedException abort) {
      Thread.currentThread().interrupt();
      System.out.println(name + " interrupted.");
    }
  }

  public static long exponential(long avg) {
    return (long) (avg * -Math.log(1 - ThreadLocalRandom.current().nextDouble()));
  }

}
Эриксон
источник
1
Три ответа в мгновение ока! Мне нравится CallbackTask, такое простое и понятное решение. Это выглядит очевидным в ретроспективе. Спасибо. Что касается других комментариев о SingleThreadedExecutor: у меня может быть тысячи очередей, которые могут иметь тысячи задач. Каждый из них должен обрабатывать свои задачи по одному, но разные очереди могут работать параллельно. Вот почему я использую один глобальный пул потоков. Я новичок в исполнителях, поэтому, пожалуйста, скажите мне, если я ошибаюсь.
Шахбаз
5
Хороший шаблон, однако, я бы использовал готовый к использованию API Guava, который обеспечит его очень хорошую реализацию.
Пьер-Анри
Разве это не превосходит цель использования Future?
позаботиться
2
@Zelphir Это был Callbackинтерфейс, который вы объявляете; не из библиотеки. В настоящее время я, вероятно, просто использую Runnable, Consumerили BiConsumer, в зависимости от того, что мне нужно, чтобы передать задание слушателю.
Эриксон
1
@Bhargav Это типично для обратных вызовов - внешний объект «перезванивает» контролирующему объекту. Вы хотите, чтобы поток, создавший задачу, блокировался до ее завершения? Тогда какова цель запуска задачи во втором потоке? Если вы разрешите потоку продолжаться, ему нужно будет повторно проверять некоторое общее состояние (возможно, в цикле, но зависит от вашей программы), пока не будет замечено обновление (логический флаг, новый элемент в очереди и т. Д.), Выполненное с помощью true обратный вызов, как описано в этом ответе. Затем он может выполнить дополнительную работу.
erickson
52

В Java 8 вы можете использовать CompletableFuture . Вот пример, который я имел в своем коде, где я использую его для извлечения пользователей из моего пользовательского сервиса, сопоставления их с моими объектами представления, а затем обновления моего представления или отображения диалога об ошибках (это приложение с графическим интерфейсом):

    CompletableFuture.supplyAsync(
            userService::listUsers
    ).thenApply(
            this::mapUsersToUserViews
    ).thenAccept(
            this::updateView
    ).exceptionally(
            throwable -> { showErrorDialogFor(throwable); return null; }
    );

Это выполняется асинхронно. Я использую два частных метода: mapUsersToUserViewsи updateView.

Matt
источник
Как можно использовать CompletableFuture с исполнителем? (чтобы ограничить число параллельных / параллельных экземпляров) Это будет подсказка: cf: submit-futuretasks-to-an-executor-почему-это-работает ?
user1767316
47

Используйте готовый API-интерфейс Guava и добавьте обратный вызов. Ср с веб-сайта :

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {
  public Explosion call() {
    return pushBigRedButton();
  }
});
Futures.addCallback(explosion, new FutureCallback<Explosion>() {
  // we want this handler to run immediately after we push the big red button!
  public void onSuccess(Explosion explosion) {
    walkAwayFrom(explosion);
  }
  public void onFailure(Throwable thrown) {
    battleArchNemesis(); // escaped the explosion!
  }
});
Pierre-Henri
источник
привет, но если я хочу остановиться после onSuccess этой темы, как я могу это сделать?
DevOps85
24

Вы можете расширить FutureTaskкласс и переопределить done()метод, а затем добавить FutureTaskобъект в ExecutorService, так что done()метод будет вызван, когда FutureTaskзавершится немедленно.

Огюст
источник
then add the FutureTask object to the ExecutorServiceне могли бы вы сказать мне, как это сделать?
Гари
@GaryGauh, чтобы узнать больше, вы можете расширить FutureTask, мы можем назвать его MyFutureTask. Затем используйте ExcutorService для отправки MyFutureTask, затем будет запущен метод run MyFutureTask, когда MyFutureTask завершит работу, и будет вызван ваш готовый метод. Здесь что-то сбивающее с толку - это два FutureTask, и фактически MyFutureTask является обычным Runnable.
Chaojun Zhong
15

ThreadPoolExecutorтакже имеет beforeExecuteи afterExecuteподключает методы, которые вы можете переопределить и использовать. Вот описание от ThreadPoolExecutor«S Javadocs .

Методы крюка

Этот класс предоставляет защищенные переопределяемые beforeExecute(java.lang.Thread, java.lang.Runnable)и afterExecute(java.lang.Runnable, java.lang.Throwable)методы, которые вызываются до и после выполнения каждой задачи. Их можно использовать для манипулирования средой исполнения; например, повторная инициализация ThreadLocals, сбор статистики или добавление записей журнала. Кроме того, метод terminated()может быть переопределен для выполнения любой специальной обработки, которую необходимо выполнить после Executorполного завершения. Если методы ловушки или обратного вызова генерируют исключения, внутренние рабочие потоки могут, в свою очередь, завершиться сбоем и внезапно завершиться.

Джем Катиккас
источник
6

Использовать CountDownLatch .

Это из java.util.concurrent и это точно способ дождаться завершения нескольких потоков, прежде чем продолжить.

Чтобы добиться эффекта обратного вызова, за которым вы ухаживаете, это требует немного дополнительной дополнительной работы. А именно, обрабатывая это самостоятельно в отдельном потоке, который использует CountDownLatchи ожидает его, затем продолжает уведомлять обо всем, что вам нужно уведомить. Нет встроенной поддержки для обратного вызова или чего-либо подобного этому эффекту.


РЕДАКТИРОВАТЬ: теперь, когда я понимаю ваш вопрос, я думаю, что вы зашли слишком далеко, без необходимости. Если вы берете регулярный SingleThreadExecutor, дайте ему все задачи, и он будет делать очереди изначально.

Ювал Адам
источник
Используя SingleThreadExecutor, каков наилучший способ узнать, что все потоки завершены? Я видел пример, который использует некоторое время! Executor.isTermination, но это не выглядит очень элегантно. Я реализовал функцию обратного вызова для каждого работника и увеличил счетчик, который работает.
Медведь
5

Если вы хотите убедиться, что никакие задачи не будут выполняться одновременно, используйте SingleThreadedExecutor . Задачи будут обрабатываться в порядке их отправки. Вам даже не нужно держать задачи, просто отправьте их в exec.

basszero
источник
2

Простой код для реализации Callbackмеханизма с использованиемExecutorService

import java.util.concurrent.*;
import java.util.*;

public class CallBackDemo{
    public CallBackDemo(){
        System.out.println("creating service");
        ExecutorService service = Executors.newFixedThreadPool(5);

        try{
            for ( int i=0; i<5; i++){
                Callback callback = new Callback(i+1);
                MyCallable myCallable = new MyCallable((long)i+1,callback);
                Future<Long> future = service.submit(myCallable);
                //System.out.println("future status:"+future.get()+":"+future.isDone());
            }
        }catch(Exception err){
            err.printStackTrace();
        }
        service.shutdown();
    }
    public static void main(String args[]){
        CallBackDemo demo = new CallBackDemo();
    }
}
class MyCallable implements Callable<Long>{
    Long id = 0L;
    Callback callback;
    public MyCallable(Long val,Callback obj){
        this.id = val;
        this.callback = obj;
    }
    public Long call(){
        //Add your business logic
        System.out.println("Callable:"+id+":"+Thread.currentThread().getName());
        callback.callbackMethod();
        return id;
    }
}
class Callback {
    private int i;
    public Callback(int i){
        this.i = i;
    }
    public void callbackMethod(){
        System.out.println("Call back:"+i);
        // Add your business logic
    }
}

вывод:

creating service
Callable:1:pool-1-thread-1
Call back:1
Callable:3:pool-1-thread-3
Callable:2:pool-1-thread-2
Call back:2
Callable:5:pool-1-thread-5
Call back:5
Call back:3
Callable:4:pool-1-thread-4
Call back:4

Ключевые примечания:

  1. Если вы хотите обрабатывать задачи в последовательности в порядке FIFO, замените newFixedThreadPool(5) наnewFixedThreadPool(1)
  2. Если вы хотите обработать следующую задачу после анализа результата callbackпредыдущей задачи, просто снимите комментарий ниже строки

    //System.out.println("future status:"+future.get()+":"+future.isDone());
  3. Вы можете заменить newFixedThreadPool()один из

    Executors.newCachedThreadPool()
    Executors.newWorkStealingPool()
    ThreadPoolExecutor

    в зависимости от вашего варианта использования.

  4. Если вы хотите обрабатывать метод обратного вызова асинхронно

    а. Передать общую ExecutorService or ThreadPoolExecutorзадачу Callable

    б. Преобразуйте ваш Callableметод в Callable/Runnableзадачу

    с. Задача обратного вызова ExecutorService or ThreadPoolExecutor

Равиндра Бабу
источник
1

Просто чтобы добавить ответ Мэтта, который помог, вот более конкретный пример, демонстрирующий использование обратного вызова.

private static Primes primes = new Primes();

public static void main(String[] args) throws InterruptedException {
    getPrimeAsync((p) ->
        System.out.println("onPrimeListener; p=" + p));

    System.out.println("Adios mi amigito");
}
public interface OnPrimeListener {
    void onPrime(int prime);
}
public static void getPrimeAsync(OnPrimeListener listener) {
    CompletableFuture.supplyAsync(primes::getNextPrime)
        .thenApply((prime) -> {
            System.out.println("getPrimeAsync(); prime=" + prime);
            if (listener != null) {
                listener.onPrime(prime);
            }
            return prime;
        });
}

Выход:

    getPrimeAsync(); prime=241
    onPrimeListener; p=241
    Adios mi amigito
Старый Джек
источник
1

Вы можете использовать реализацию Callable так, чтобы

public class MyAsyncCallable<V> implements Callable<V> {

    CallbackInterface ci;

    public MyAsyncCallable(CallbackInterface ci) {
        this.ci = ci;
    }

    public V call() throws Exception {

        System.out.println("Call of MyCallable invoked");
        System.out.println("Result = " + this.ci.doSomething(10, 20));
        return (V) "Good job";
    }
}

где CallbackInterface является чем-то очень простым, как

public interface CallbackInterface {
    public int doSomething(int a, int b);
}

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

ExecutorService ex = Executors.newFixedThreadPool(2);

MyAsyncCallable<String> mac = new MyAsyncCallable<String>((a, b) -> a + b);
ex.submit(mac);
Дипика Ананд
источник
1

Это расширение ответа Пача с использованием Guava ListenableFuture.

В частности, Futures.transform()возвраты ListenableFutureмогут быть использованы для цепочки асинхронных вызовов. Futures.addCallback()возвращает void, поэтому не может использоваться для цепочки, но хорошо для обработки успеха / неудачи при асинхронном завершении.

// ListenableFuture1: Open Database
ListenableFuture<Database> database = service.submit(() -> openDatabase());

// ListenableFuture2: Query Database for Cursor rows
ListenableFuture<Cursor> cursor =
    Futures.transform(database, database -> database.query(table, ...));

// ListenableFuture3: Convert Cursor rows to List<Foo>
ListenableFuture<List<Foo>> fooList =
    Futures.transform(cursor, cursor -> cursorToFooList(cursor));

// Final Callback: Handle the success/errors when final future completes
Futures.addCallback(fooList, new FutureCallback<List<Foo>>() {
  public void onSuccess(List<Foo> foos) {
    doSomethingWith(foos);
  }
  public void onFailure(Throwable thrown) {
    log.error(thrown);
  }
});

ПРИМЕЧАНИЕ. Помимо цепочки асинхронных задач, Futures.transform()вы также можете планировать каждую задачу для отдельного исполнителя (в этом примере не показано).

bcorso
источник
Это выглядит довольно мило.
Кайзер