Javaエグゼキューター:タスクが完了したときに、ブロックせずに通知する方法


154

エクゼキューターサービスに送信する必要があるタスクでいっぱいのキューがあるとします。一度に1つずつ処理してほしい。私が考えることができる最も簡単な方法は:

  1. キューからタスクを取得する
  2. 実行者に提出する
  3. 返されたFutureで.getを呼び出し、結果が利用可能になるまでブロックします
  4. キューから別のタスクを実行...

ただし、完全にブロックしないようにしています。タスクを一度に1つずつ処理する必要のある10,000のキューがある場合、それらのほとんどがブロックされたスレッドを保持しているため、スタック領域が不足します。

私が望むのは、タスクを送信し、タスクが完了したときに呼び出されるコールバックを提供することです。そのコールバック通知をフラグとして使用して、次のタスクを送信します。(functionaljavaとjetlangはこのような非ブロッキングアルゴリズムを使用しているようですが、コードを理解できません)

JDKのjava.util.concurrentを使用してそれを行うにはどうすればよいですか。

(これらのタスクを提供するキュー自体がブロックする可能性がありますが、それは後で取り組む問題です)

回答:


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

}

では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
瞬く間に3つの答え!私はCallbackTaskが好きです。このようなシンプルで簡単なソリューションです。振り返ってみると明らかです。ありがとう。SingleThreadedExecutorに関する他のコメントに関して:私は何千ものタスクを持つかもしれない何千ものキューを持っているかもしれません。それらのそれぞれが一度に1つずつタスクを処理する必要がありますが、異なるキューが並行して動作できます。そのため、単一のグローバルスレッドプールを使用しています。私はエグゼキューターが初めてなので、間違えたら教えてください。
Shahbaz

5
良いパターンですが、非常に優れた実装を提供するGuavaのリッスン可能な将来のAPI使用します。
Pierre-Henri

これはFutureを使用する目的に勝るものではありませんか?
2015年

2
@ZelphirそれはCallbackあなたが宣言したインターフェースでした。ライブラリからではありません。今日では私はおそらくちょうど使用したいRunnableConsumerまたはBiConsumer、私はリスナーに戻ってタスクから渡すために必要なものに依存します。
エリクソン

1
@Bhargavこれは典型的なコールバックです。外部エンティティが制御エンティティに「コールバック」します。タスクが終了するまで、タスクを作成したスレッドをブロックしますか?次に、2番目のスレッドでタスクを実行する目的は何ですか?スレッドの継続を許可する場合は、trueによって行われた更新(ブールフラグ、キュー内の新しいアイテムなど)に気づくまで、共有状態(おそらくループ内ですが、プログラムによって異なります)を繰り返し確認する必要がありますこの回答に記載されているコールバック。その後、いくつかの追加作業を実行できます。
エリクソン2016年

52

Java 8ではCompletableFutureを使用できます。これが私のコードにあった例です。これを使用して、ユーザーサービスからユーザーをフェッチし、それらをビューオブジェクトにマップして、ビューを更新するか、エラーダイアログを表示します(これはGUIアプリケーションです)。

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

非同期で実行されます。私は2つのプライベートメソッドを使用しています:mapUsersToUserViewsupdateView


executorでCompletableFutureをどのように使用しますか?(並行/並列インスタンスの数を制限するため)これはヒントになりますか?cf:submitting-futuretasks-to-an-executor-why-does-it-work
user1767316

47

使用グアバの聴き将来のAPIとコールバックを追加します。Cf. ウェブサイトから:

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 2015年

24

FutureTaskクラスを拡張してdone()メソッドをオーバーライドし、次にFutureTaskオブジェクトをに追加するとExecutorServicedone()メソッドはFutureTask完了時にすぐに呼び出されます。


then add the FutureTask object to the ExecutorServiceどうすればいいですか?
ゲイリーガウ2017年

@GaryGauh これを参照して、 FutureTask 拡張できる詳細を確認してください。MyFutureTaskと呼ぶこともあります。次に、ExcutorServiceを使用してMyFutureTaskを送信すると、MyFutureTaskのrunメソッドが実行され、MyFutureTaskが終了すると、doneメソッドが呼び出されます。
Chaojun Zhong 2017

15

ThreadPoolExecutorにはbeforeExecuteafterExecuteオーバーライドして使用できるフックメソッドもあります。以下は、JavadocからThreadPoolExecutorの説明です。

フックメソッド

このクラスは、各タスクの実行前と実行後に呼び出される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を使用して、すべてのスレッドが完了したことを知る最良の方法は何ですか?while!executor.isTerminatedを使用する例を見ましたが、これは非常にエレガントに見えません。各ワーカーにコールバック機能を実装し、機能するカウントを増やします。
2014

5

タスクが同時に実行されないようにする場合は、SingleThreadedExecutorを使用します。タスクは、送信された順に処理されます。タスクを保持する必要すらなく、execに送信するだけです。


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. コールバックメソッドを非同期で処理する場合

    a。共有を渡すExecutorService or ThreadPoolExecutorを呼び出し可能タスクに

    b。あなたの変換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

これは、Guavaを使用したPacheの回答の拡張ListenableFutureです。

特に、Futures.transform()return ListenableFuturesoは非同期呼び出しをチェーンするために使用できます。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()、別のエグゼキューターで各タスクをスケジュールすることもできます(この例には示されていません)。


これはかなりいいようです。
カイザー2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.