Người thực thi Java: làm thế nào để được thông báo, mà không chặn, khi một tác vụ hoàn thành?


154

Nói rằng tôi có một hàng đợi đầy đủ các nhiệm vụ mà tôi cần gửi đến một dịch vụ thực thi. Tôi muốn họ xử lý từng cái một. Cách đơn giản nhất tôi có thể nghĩ đến là:

  1. Nhận một nhiệm vụ từ hàng đợi
  2. Gửi nó cho người thi hành
  3. Gọi .get trên Tương lai được trả về và chặn cho đến khi có kết quả
  4. Nhận một nhiệm vụ khác từ hàng đợi ...

Tuy nhiên, tôi đang cố gắng tránh chặn hoàn toàn. Nếu tôi có 10.000 hàng đợi như vậy, cần xử lý từng nhiệm vụ của chúng, tôi sẽ hết dung lượng ngăn xếp vì hầu hết chúng sẽ giữ các luồng bị chặn.

Những gì tôi muốn là gửi một nhiệm vụ và cung cấp một cuộc gọi lại được gọi khi nhiệm vụ hoàn thành. Tôi sẽ sử dụng thông báo gọi lại đó làm cờ để gửi tác vụ tiếp theo. (functionjava và jetlang rõ ràng sử dụng các thuật toán không chặn như vậy, nhưng tôi không thể hiểu mã của chúng)

Làm thế nào tôi có thể làm điều đó bằng cách sử dụng java.util.conc hiện của JDK, viết tắt dịch vụ thực thi của riêng tôi?

(hàng đợi cung cấp cho tôi các tác vụ này có thể tự chặn, nhưng đó là vấn đề cần giải quyết sau)

Câu trả lời:


146

Xác định giao diện gọi lại để nhận bất kỳ tham số nào bạn muốn chuyển qua trong thông báo hoàn thành. Sau đó gọi nó vào cuối nhiệm vụ.

Bạn thậm chí có thể viết một trình bao bọc chung cho các tác vụ Runnable và gửi chúng cho ExecutorService. Hoặc, xem bên dưới để biết cơ chế được tích hợp trong 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();
  }

}

Với CompletableFuture, Java 8 bao gồm một phương tiện phức tạp hơn để soạn thảo các đường ống nơi các quy trình có thể được hoàn thành không đồng bộ và có điều kiện. Đây là một ví dụ đầy đủ nhưng đầy đủ của thông báo.

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
Ba câu trả lời trong chớp mắt! Tôi thích CallbackTask, một giải pháp đơn giản và dễ hiểu. Nó có vẻ rõ ràng khi nhìn lại. Cảm ơn. Về những ý kiến ​​khác về SingleThreadedExecutor: Tôi có thể có hàng ngàn hàng đợi có thể có hàng ngàn nhiệm vụ. Mỗi người trong số họ cần xử lý từng nhiệm vụ một, nhưng các hàng đợi khác nhau có thể hoạt động song song. Đó là lý do tại sao tôi đang sử dụng một luồng toàn cầu duy nhất. Tôi mới làm quen với giám đốc điều hành vì vậy xin vui lòng cho tôi biết nếu tôi nhầm.
Shahbaz

5
Tuy nhiên, tôi sẽ sử dụng API tương lai có thể nghe được của Guava để cung cấp triển khai rất tốt cho nó.
Pierre-Henri

Điều này không đánh bại mục đích sử dụng Tương lai?
takecare

2
@Zelphir Đó là một Callbackgiao diện mà bạn khai báo; không phải từ một thư viện. Ngày nay có lẽ tôi chỉ sử dụng Runnable, Consumerhoặc BiConsumer, tùy thuộc vào những gì tôi cần chuyển lại từ nhiệm vụ cho người nghe.
erickson

1
@Bhargav Đây là điển hình của các cuộc gọi lại, một thực thể bên ngoài "gọi lại" cho thực thể kiểm soát. Bạn có muốn luồng đã tạo tác vụ chặn cho đến khi tác vụ kết thúc không? Vậy thì mục đích của việc chạy tác vụ trên luồng thứ hai là gì? Nếu bạn cho phép luồng tiếp tục, nó sẽ cần liên tục kiểm tra một số trạng thái được chia sẻ (có thể trong một vòng lặp, nhưng phụ thuộc vào chương trình của bạn) cho đến khi thông báo cập nhật (cờ boolean, mục mới trong hàng đợi, v.v.) được tạo bởi true gọi lại như mô tả trong câu trả lời này. Sau đó nó có thể thực hiện một số công việc bổ sung.
erickson

52

Trong Java 8, bạn có thể sử dụng CompleteableFuture . Đây là một ví dụ tôi có trong mã của mình, nơi tôi đang sử dụng nó để tìm nạp người dùng từ dịch vụ người dùng của mình, ánh xạ chúng tới các đối tượng xem của tôi và sau đó cập nhật chế độ xem của tôi hoặc hiển thị hộp thoại lỗi (đây là ứng dụng GUI):

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

Nó thực thi không đồng bộ. Tôi đang sử dụng hai phương pháp riêng tư: mapUsersToUserViewsupdateView.


Làm thế nào một người sẽ sử dụng CompleteableFuture với một người thi hành? (để giới hạn số lượng phiên bản đồng thời / song song) Đây có phải là một gợi ý: cf: submit-futuret task-to-an-execor-why-does-it-work ?
dùng1767316

47

Sử dụng API tương lai có thể nghe của Guava và thêm một cuộc gọi lại. Cf. từ trang web:

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

xin chào, nhưng nếu tôi muốn dừng lại sau khi onSuccess Chủ đề này thì tôi phải làm thế nào?
DevOps85

24

Bạn có thể mở rộng FutureTasklớp và ghi đè done()phương thức, sau đó thêm FutureTaskđối tượng vào ExecutorService, vì vậy done()phương thức sẽ gọi khi FutureTaskhoàn thành ngay lập tức.


then add the FutureTask object to the ExecutorService, bạn có thể vui lòng cho tôi biết làm thế nào để làm điều này?
Gary Gauh

@GaryGauh xem phần này để biết thêm thông tin bạn có thể mở rộng FutureTask, chúng tôi có thể gọi nó là MyFutureTask. Sau đó, sử dụng ExcutorService để gửi MyFutureTask, sau đó phương thức chạy của MyFutureTask sẽ chạy, khi MyFutureTask kết thúc phương thức đã hoàn thành của bạn sẽ được gọi.
Chaojun Zhong

15

ThreadPoolExecutorcũng có beforeExecuteafterExecutemóc phương thức mà bạn có thể ghi đè và sử dụng. Dưới đây là mô tả từ ThreadPoolExecutor's Javadocs .

Phương pháp móc

Lớp này cung cấp bảo vệ overridable beforeExecute(java.lang.Thread, java.lang.Runnable)afterExecute(java.lang.Runnable, java.lang.Throwable)các phương thức được gọi trước và sau khi thực hiện từng tác vụ. Chúng có thể được sử dụng để thao túng môi trường thực thi; ví dụ: sắp xếp lại ThreadLocals, thu thập số liệu thống kê hoặc thêm các mục nhật ký. Ngoài ra, phương thức terminated()có thể được ghi đè để thực hiện bất kỳ xử lý đặc biệt nào cần được thực hiện sau khi Executorkết thúc hoàn toàn. Nếu các phương thức hook hoặc callback ném ngoại lệ, các luồng công nhân nội bộ có thể lần lượt bị lỗi và đột ngột chấm dứt.


6

Sử dụng a CountDownLatch.

Đó là từ java.util.concurrentvà đó chính xác là cách chờ đợi một số luồng hoàn thành thực thi trước khi tiếp tục.

Để đạt được hiệu ứng gọi lại mà bạn đang chăm sóc, điều đó đòi hỏi một chút công việc phụ. Cụ thể, tự xử lý việc này trong một luồng riêng biệt sử dụng CountDownLatchvà chờ đợi trên đó, sau đó tiếp tục thông báo bất cứ điều gì bạn cần thông báo. Không có hỗ trợ riêng cho gọi lại, hoặc bất cứ điều gì tương tự như hiệu ứng đó.


EDIT: bây giờ tôi hiểu thêm câu hỏi của bạn, tôi nghĩ rằng bạn đang đi quá xa, không cần thiết. Nếu bạn thường xuyên SingleThreadExecutor, hãy giao cho nó tất cả các nhiệm vụ và nó sẽ thực hiện việc xếp hàng một cách tự nhiên.


Sử dụng SingleThreadExecutor cách tốt nhất để biết rằng tất cả các luồng đã hoàn thành là gì? Tôi đã thấy một đề thi sử dụng một thời gian! Thực thi. Được xác định nhưng điều này có vẻ không thanh lịch. Tôi đã triển khai một tính năng gọi lại cho mỗi công nhân và tăng số đếm hoạt động.
Gấu

5

Nếu bạn muốn đảm bảo rằng không có tác vụ nào sẽ chạy cùng một lúc thì hãy sử dụng SingleThreadedExecutor . Các nhiệm vụ sẽ được xử lý theo thứ tự được đệ trình. Bạn thậm chí không cần phải giữ các nhiệm vụ, chỉ cần gửi chúng cho người thực thi.


2

Mã đơn giản để thực hiện Callbackcơ chế sử dụngExecutorService

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
    }
}

đầu ra:

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

Lưu ý chính:

  1. Nếu bạn muốn xử lý các tác vụ theo trình tự theo thứ tự FIFO, hãy thay thế newFixedThreadPool(5) bằngnewFixedThreadPool(1)
  2. Nếu bạn muốn xử lý tác vụ tiếp theo sau khi phân tích kết quả từ callbacktác vụ trước, chỉ cần bỏ bình luận bên dưới dòng

    //System.out.println("future status:"+future.get()+":"+future.isDone());
  3. Bạn có thể thay thế newFixedThreadPool()bằng một trong

    Executors.newCachedThreadPool()
    Executors.newWorkStealingPool()
    ThreadPoolExecutor

    tùy thuộc vào trường hợp sử dụng của bạn.

  4. Nếu bạn muốn xử lý phương thức gọi lại không đồng bộ

    a. Vượt qua một ExecutorService or ThreadPoolExecutornhiệm vụ được chia sẻ

    b. Chuyển đổi Callablephương thức của bạn thành Callable/Runnablenhiệm vụ

    c. Đẩy tác vụ gọi lại đến ExecutorService or ThreadPoolExecutor


1

Chỉ cần thêm vào câu trả lời của Matt, điều này có ích, đây là một ví dụ rõ ràng hơn để cho thấy việc sử dụng một cuộc gọi lại.

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

Đầu ra là:

    getPrimeAsync(); prime=241
    onPrimeListener; p=241
    Adios mi amigito

1

Bạn có thể sử dụng triển khai Callable sao cho

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";
    }
}

trong đó CallbackInterface là một cái gì đó rất cơ bản như

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

và bây giờ lớp chính sẽ trông như thế này

ExecutorService ex = Executors.newFixedThreadPool(2);

MyAsyncCallable<String> mac = new MyAsyncCallable<String>((a, b) -> a + b);
ex.submit(mac);

1

Đây là phần mở rộng cho câu trả lời của Pache khi sử dụng Guava ListenableFuture.

Cụ thể, Futures.transform()trả về ListenableFuturevì vậy có thể được sử dụng để xâu chuỗi các cuộc gọi async. Futures.addCallback()trả về void, do đó không thể được sử dụng để xâu chuỗi, nhưng tốt cho việc xử lý thành công / thất bại khi hoàn thành async.

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

LƯU Ý: Ngoài việc xâu chuỗi các tác vụ không đồng bộ, Futures.transform()cũng cho phép bạn lên lịch cho từng tác vụ trên một trình thực thi riêng (Không được hiển thị trong ví dụ này).


Điều này có vẻ khá tốt đẹp.
kaiser
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.