Виконавці Java: як отримувати сповіщення без блокування, коли завдання виконується?


154

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

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

Однак я намагаюся уникати блокування повністю. Якщо у мене є 10 000 таких черг, для яких потрібно обробляти свої завдання одна за одною, у мене залишиться місця у стеці, оскільки більшість з них буде триматися на заблокованих потоках.

Мені хотілося б подати завдання та надіслати зворотній дзвінок, який викликається, коли завдання виконане. Я буду використовувати це повідомлення про зворотний дзвінок як прапор для надсилання наступного завдання. (Функціонава і jetlang, мабуть, використовують такі неблокуючі алгоритми, але я не можу зрозуміти їх код)

Як я можу це зробити за допомогою програми java.util.contil JDK, якщо я не написав власну службу виконавця?

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

Відповіді:


146

Визначте інтерфейс зворотного дзвінка, щоб отримати будь-які параметри, які ви хочете передати у повідомленні про завершення. Потім введіть його в кінці завдання.

Ви навіть можете написати загальну обгортку для завдань, що виконується, і надіслати їх 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();
  }

}

З CompletableFuture, Java 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, що забезпечує його дуже гарну реалізацію.
П’єр-Анрі

це не перемагає мету використання майбутнього?
берегти

2
@Zelphir CallbackВи заявляли про інтерфейс, який ви заявляєте; не з бібліотеки. Зараз я б , напевно , просто використовувати Runnable, Consumerабо BiConsumer, в залежності від того, що мені потрібно передати назад від завдання до слухача.
erickson

1
@Bhargav Це типово для зворотних викликів - зовнішня особа "передзвонює" керуючому об'єкту. Ви хочете, щоб нитка, яка створила завдання, блокувалася, поки завдання не закінчиться? Тоді яка мета у виконанні завдання на другій темі? Якщо ви дозволите продовження потоку, йому потрібно буде кілька разів перевіряти деякий загальний стан (можливо, в циклі, але це залежить від вашої програми), поки він не помітить оновлення (прапор булева, новий елемент у черзі тощо), зроблене істинним зворотний виклик, як описано в цій відповіді. Потім він може виконати якусь додаткову роботу.
erickson

52

У Java 8 ви можете використовувати CompletableFuture . Ось приклад, який я мав у своєму коді, де я використовую його для отримання користувачів з моєї служби користувачів, перенесення їх на об’єкти перегляду, а потім оновлення мого перегляду або показ діалогового вікна помилки (це програма GUI):

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

Він виконується асинхронно. Я використовую два приватні методи: mapUsersToUserViewsі updateView.


Як можна використовувати CompletableFuture з виконавцем? (щоб обмежити кількість примірників / паралельних екземплярів) Це був би натяк: cf: подання-futuretasks-to-an-исполнителю, чому це працює ?
користувач1767316

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

привіт, але якщо я хочу зупинитись після onSuccess цієї теми, як я можу це зробити?
DevOps85

24

Ви можете розширити FutureTaskклас і замінити done()метод, а потім додати FutureTaskоб'єкт до ExecutorService, так що done()метод буде викликати, коли FutureTaskзавершено негайно.


then add the FutureTask object to the ExecutorService, чи не могли б ви сказати мені, як це зробити?
Gary Gauh

@GaryGauh див. Це для отримання додаткової інформації, яку ви можете розширити FutureTask, ми можемо назвати це MyFutureTask. Потім використовуйте програму ExcutorService, щоб подати MyFutureTask, тоді буде запущений метод запуску MyFutureTask, коли MyFutureTask закінчив ваш готовий метод, буде називатися.
Чаоджун Чжун

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

Використовуйте a CountDownLatch.

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

Щоб досягти ефекту зворотного дзвінка, за яким ви доглядаєте, це вимагає трохи додаткових робіт. А саме, обробляючи це самостійно в окремому потоці, який використовує CountDownLatchі не чекає на нього, а потім продовжує повідомляти про все, що вам потрібно повідомити. Немає вбудованої підтримки для зворотного дзвінка або чогось подібного до цього ефекту.


РЕДАКЦІЯ: тепер, коли я ще більше розумію ваше запитання, я думаю, що ви надто далеко, непотрібно. Якщо ви берете звичайну SingleThreadExecutor, дайте їй усі завдання, і вона виконає чергу вдома.


Використовуючи SingleThreadExecutor, який найкращий спосіб дізнатися, що всі нитки виконані? Я бачив приклад, який використовує деякий час! Executor.isTerminated, але це не дуже елегантно. Я реалізував функцію зворотного виклику для кожного працівника і збільшував кількість, яка працює.
Ведмідь

5

Якщо ви хочете переконатися, що жодне завдання не буде виконуватися одночасно, тоді використовуйте SingleThreadedExecutor . Завдання опрацьовуватимуться в порядку, до якого надсилаються. Вам навіть не потрібно тримати завдання, просто надішліть їх до виконання.


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завдання

    c. Натисніть завдання зворотного дзвінка на 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

Це розширення до відповіді Паче, використовуючи відповіді Гуави 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()ви також можете планувати кожне завдання на окремому виконавцю (Не показано в цьому прикладі).


Це здається досить приємним.
кайзер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.