ตัวดำเนินการ Java: จะได้รับการแจ้งเตือนอย่างไรโดยไม่ต้องปิดกั้นเมื่องานเสร็จสมบูรณ์?


160

สมมติว่าฉันมีคิวงานเต็มไปหมดซึ่งฉันต้องส่งไปที่บริการปฏิบัติการ ฉันต้องการให้พวกเขาประมวลผลทีละรายการ วิธีที่ง่ายที่สุดที่ฉันคิดได้คือ:

  1. รับงานจากคิว
  2. ส่งไปยังผู้ดำเนินการ
  3. โทร. รับอนาคตที่ส่งคืนและบล็อกจนกว่าผลลัพธ์จะพร้อมใช้งาน
  4. รับภารกิจอื่นจากคิว ...

อย่างไรก็ตามฉันพยายามหลีกเลี่ยงการบล็อกโดยสิ้นเชิง หากฉันมี 10,000 คิวดังกล่าวซึ่งจำเป็นต้องมีการประมวลผลทีละงานฉันจะไม่มีพื้นที่สแต็กเนื่องจากส่วนใหญ่จะยึดไว้กับเธรดที่ถูกบล็อก

สิ่งที่ฉันต้องการคือส่งงานและให้การติดต่อกลับซึ่งจะเรียกว่าเมื่องานเสร็จสมบูรณ์ ฉันจะใช้การแจ้งเตือนการโทรกลับเป็นธงในการส่งงานต่อไป (เห็นได้ชัดว่า functionaljava และ jetlang ใช้อัลกอริทึมที่ไม่ปิดกั้นดังกล่าว แต่ฉันไม่เข้าใจรหัสของพวกเขา)

ฉันจะทำเช่นนั้นได้อย่างไรโดยใช้ java.util.concurrent ของ JDK ซึ่งย่อมาจากบริการเรียกใช้งานของฉัน

(คิวที่ให้อาหารฉันงานเหล่านี้เองอาจบล็อก แต่นั่นเป็นปัญหาที่ต้องแก้ไขในภายหลัง)

คำตอบ:


148

กำหนดอินเทอร์เฟซการโทรกลับเพื่อรับพารามิเตอร์ที่คุณต้องการส่งผ่านในการแจ้งเตือนการเสร็จสิ้น จากนั้นเรียกใช้เมื่อสิ้นสุดภารกิจ

คุณยังสามารถเขียนกระดาษห่อทั่วไปสำหรับงาน 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: ฉันอาจมีหลายพันคิวซึ่งอาจมีหลายพันงาน แต่ละคนต้องประมวลผลงานทีละคิว แต่คิวต่างกันสามารถทำงานควบคู่กันได้ นั่นเป็นเหตุผลที่ฉันใช้เธรดพูลส่วนกลางเดียว ฉันยังใหม่กับผู้ปฏิบัติการดังนั้นโปรดบอกฉันว่าฉันเข้าใจผิดหรือเปล่า
Shahbaz

5
รูปแบบที่ดีอย่างไรก็ตามฉันจะใช้API ในอนาคตที่ฟังได้ของ Guavaซึ่งให้การใช้งานที่ดีมาก
Pierre-Henri

สิ่งนี้ไม่ผิดวัตถุประสงค์ของการใช้ Future?
takecare

2
@ Zelphir มันเป็นCallbackอินเทอร์เฟซที่คุณประกาศ; ไม่ได้มาจากห้องสมุด ปัจจุบันฉันอาจจะเพียงแค่ใช้Runnable, ConsumerหรือBiConsumerขึ้นอยู่กับสิ่งที่ฉันต้องการที่จะผ่านกลับมาจากงานที่จะฟัง
erickson

1
@Bhargav นี่เป็นเรื่องปกติของการโทรกลับ - เอนทิตีภายนอก "โทรกลับ" ไปยังหน่วยงานที่ควบคุม คุณต้องการให้เธรดที่สร้างงานบล็อกจนกว่างานจะเสร็จสิ้นหรือไม่ แล้วมีจุดประสงค์อะไรในการรันงานบนเธรดที่สอง? หากคุณอนุญาตให้เธรดดำเนินการต่อจะต้องตรวจสอบสถานะที่ใช้ร่วมกันซ้ำ ๆ (อาจอยู่ในลูป แต่ขึ้นอยู่กับโปรแกรมของคุณ) จนกว่าจะสังเกตเห็นการอัปเดต (แฟล็กบูลีนรายการใหม่ในคิว ฯลฯ ) ที่ทำโดย true โทรกลับตามที่อธิบายไว้ในคำตอบนี้ จากนั้นสามารถทำงานเพิ่มเติมได้
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 กับผู้ปฏิบัติการได้อย่างไร? (เพื่อ จำกัด จำนวนอินสแตนซ์ concurent / parallel) นี่จะเป็นคำใบ้ไหม : cf: submit-futuretasks-to-an-executor-why-does-it-work ?
user1767316

48

ใช้API ในอนาคตที่ฟังได้ของ Guavaและเพิ่มการโทรกลับ 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 this Thread ฉันจะทำอย่างไร?
DevOps85

25

คุณสามารถขยายFutureTaskคลาสและแทนที่done()เมธอดแล้วเพิ่มFutureTaskอ็อบเจกต์ลงExecutorServiceในdone()เมธอดดังนั้นเมธอดจะเรียกใช้เมื่อFutureTaskเสร็จสิ้นทันที


then add the FutureTask object to the ExecutorServiceคุณช่วยบอกฉันได้ไหมว่าต้องทำอย่างไร
Gary Gauh

@GaryGauh ดูข้อมูลเพิ่มเติมที่คุณสามารถขยาย FutureTask เราอาจเรียกว่า MyFutureTask จากนั้นใช้ ExcutorService เพื่อส่ง MyFutureTask จากนั้นวิธีการรันของ 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.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/Runnableงาน

    ค. กดงานโทรกลับไปที่ ExecutorService or ThreadPoolExecutor


2

นี้เป็นส่วนขยายที่จะตอบ Pache ListenableFutureโดยใช้ฝรั่งของ

โดยเฉพาะอย่างยิ่งFutures.transform()ผลตอบแทนที่ListenableFutureสามารถใช้เพื่อเชื่อมโยงการโทรแบบ async Futures.addCallback()ผลตอบแทนvoidจึงไม่สามารถใช้สำหรับการผูกมัด แต่เหมาะสำหรับการจัดการกับความสำเร็จ / ความล้มเหลวในการเสร็จสิ้นแบบ 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);
  }
});

หมายเหตุ:นอกเหนือจากการเชื่อมโยงงาน async แล้วFutures.transform()ยังช่วยให้คุณสามารถกำหนดเวลาแต่ละงานบนตัวดำเนินการแยกต่างหาก (ไม่แสดงในตัวอย่างนี้)


ดูเหมือนจะดี
kaiser

1

เพียงเพื่อเพิ่มคำตอบของ Matt ซึ่งช่วยได้นี่คือตัวอย่างที่ละเอียดมากขึ้นเพื่อแสดงการใช้การโทรกลับ

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);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.