Java yürütücüler: Bir görev tamamlandığında engelleme olmadan nasıl bildirimde bulunulur?


154

Bir yürütme hizmetine göndermem gereken görevlerle dolu bir kuyruğum olduğunu varsayalım. Birer birer işlenmelerini istiyorum. Düşünebildiğim en basit yol:

  1. Kuyruktan bir görev al
  2. İcracıya gönder
  3. Döndürülen Gelecek'i arayın ve bir sonuç elde edilinceye kadar engelleyin.
  4. Kuyruktan başka bir görev al ...

Ancak, tamamen engelleme önlemek için çalışıyorum. Görevleri birer birer işlenmesi gereken 10.000 sıraya sahipsem, yığın alanınız tükenir çünkü çoğu engellenen iş parçacıklarını tutacaktır.

Ne istiyorum bir görev göndermek ve görev tamamlandığında çağrılan bir geri arama sağlamaktır. Bir sonraki görevi göndermek için geri arama bildirimini bayrak olarak kullanacağım. (fonksiyoneljava ve jetlang görünüşe göre bu tür engelleme olmayan algoritmaları kullanır, ancak kodlarını anlayamıyorum)

Bunu kendi yürütme hizmetimi yazmamak için JDK'nın java.util.concurrent kullanarak nasıl yapabilirim?

(bu görevleri bana besleyen kuyruğun kendisi engellenebilir, ancak bu daha sonra ele alınması gereken bir konudur)

Yanıtlar:


146

Tamamlama bildiriminde iletmek istediğiniz parametreleri almak için bir geri arama arabirimi tanımlayın. Sonra görev sonunda çağırın.

Runnable görevleri için genel bir sargı yazıp bunları gönderebilirsiniz ExecutorService. Veya Java 8'de yerleşik bir mekanizma için aşağıya bakın.

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

}

Bununla birlikte CompletableFuture, Java 8, işlemlerin eşzamansız ve koşullu olarak tamamlanabileceği boru hatlarını oluşturmak için daha ayrıntılı bir araç içermiştir. İşte ihtilaflı ama eksiksiz bir bildirim örneği.

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
Bir göz açıp kapayıncaya kadar üç cevap! CallbackTask'ı seviyorum, böyle basit ve basit bir çözüm. Geçmişe bakıldığında bariz görünüyor. Teşekkürler. Diğerleri hakkında SingleThreadedExecutor hakkında yorumlar: Binlerce görevi olabilen binlerce kuyruğum olabilir. Her birinin görevlerini birer birer işlemesi gerekir, ancak farklı kuyruklar paralel olarak çalışabilir. Bu yüzden tek bir global threadpool kullanıyorum. İcracılara yeniyim, bu yüzden lütfen yanılmış olup olmadığımı söyle.
Shahbaz

5
İyi bir desen, ancak Guava'nın çok iyi bir uygulama sağlayan dinlenebilir gelecek API'sini kullanırdım.
Pierre-Henri

Bu, Gelecek'i kullanma amacını aşmıyor mu?
takecare

2
@Zelphir Bildirdiğiniz bir Callbackarayüzdü; kütüphaneden değil. Bugünlerde Runnable, muhtemelen görevden dinleyiciye geçmem gereken şeylere bağlı olarak Consumer, ya da kullanacağım BiConsumer.
erickson

1
@Bhargav Bu, geri aramaların tipik bir örneğidir; harici bir varlık, kontrol eden işletmeye "geri döner". Görevi oluşturan iş parçacığının görev bitinceye kadar engellenmesini ister misiniz? O zaman görevi ikinci bir iş parçacığında çalıştırmanın ne amacı var? İş parçacığının devam etmesine izin verirseniz, true tarafından yapılan bir güncelleme (boole bayrağı, kuyruktaki yeni öğe, vb.) Fark edene kadar bazı paylaşılan durumları (muhtemelen bir döngüde, ancak programınıza bağlı olarak) tekrar tekrar kontrol etmeniz gerekir. Bu cevapta açıklandığı gibi geri arama. Daha sonra bazı ek işler yapabilir.
erickson

52

Java 8'de CompletableFuture kullanabilirsiniz . Kodumda, kullanıcı hizmetimden kullanıcıları almak, bunları görünüm nesnelerimle eşlemek ve daha sonra görünümümü güncellemek veya bir hata iletişim kutusu göstermek için kullandığım bir örnek var (bu bir GUI uygulamasıdır):

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

Zaman uyumsuz olarak yürütülür. İki özel yöntem kullanıyorum: mapUsersToUserViewsve updateView.


Bir icracı ile CompletableFuture nasıl kullanılır? (eşzamanlı / paralel örneklerin sayısını sınırlamak için) Bu bir ipucu olabilir mi: cf: bir yürütücüye-gelecek görevler gönderme-neden-işe yarar ?
user1767316

47

Guava'nın gelecekteki dinlenebilir API'sını kullanın ve geri arama ekleyin. Krş web sitesinden:

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

merhaba, ama ben onSuccess sonra bu Konu durdurmak istiyorum nasıl yapabilirim?
DevOps85

24

FutureTaskSınıfı genişletebilir ve done()yöntemi geçersiz kılabilir , ardından FutureTasknesneyi öğesine ekleyebilirsiniz ExecutorService, böylece done()yöntem FutureTaskhemen tamamlandığında çağrılır.


then add the FutureTask object to the ExecutorService, lütfen bunu nasıl yapacağımı söyleyebilir misin?
Gary Gauh

@GaryGauh daha fazla bilgi için bunu görün. Daha sonra MyFutureTask'ı göndermek için ExcutorService'i kullanın, MyFutureTask'ın çalışma yöntemi MyFutureTask bitirdiğiniz yöntem çağrıldığında çalışacaktır. Kafa karıştırıcı bir şey iki FutureTask ve aslında MyFutureTask normal bir Runnable.
Chaojun Zhong

15

ThreadPoolExecutorAyrıca sahiptir beforeExecuteve afterExecutegeçersiz kılmak ve yararlanabilirler o kanca yöntemleri. İşte ThreadPoolExecutor's Javadocs açıklaması .

Kanca yöntemleri

Bu sınıf, korumalı geçersiz kılınabilir beforeExecute(java.lang.Thread, java.lang.Runnable)ve afterExecute(java.lang.Runnable, java.lang.Throwable)her görevin yürütülmesinden önce ve sonra çağrılan yöntemler sağlar . Bunlar yürütme ortamını değiştirmek için kullanılabilir; örneğin, yeniden başlatma ThreadLocals, istatistik toplama veya günlük girişleri ekleme. Ayrıca, tamamen sonlandırıldıktan terminated()sonra yapılması gereken herhangi bir özel işlemi gerçekleştirmek için yöntem geçersiz kılınabilir Executor. Kanca veya geri arama yöntemleri istisnalar atarsa, dahili çalışan iş parçacıkları başarısız olabilir ve aniden sonlanabilir.


6

A kullanın CountDownLatch.

Bu java.util.concurrent, devam etmeden önce birkaç iş parçacığının yürütmeyi tamamlamasını beklemenin tam yoludur.

Baktığınız geri arama etkisini elde etmek için biraz daha fazla ek iş gerektirir. Yani, bunu kendiniz kullanarak CountDownLatchve üzerinde bekleyen ayrı bir iş parçacığında işlemek , daha sonra bildirmeniz gereken her şeyi bildirmeye devam eder. Geri arama için yerel bir destek veya bu etkiye benzer bir şey yoktur.


EDIT: şimdi sorunuzu daha iyi anladığım için, gereksiz yere çok uzağa ulaştığınızı düşünüyorum. Düzenli olarak alırsanız SingleThreadExecutor, tüm görevleri verin ve kuyruğu doğal olarak yapar.


SingleThreadExecutor kullanarak tüm evrelerin tamamlandığını bilmenin en iyi yolu nedir? Bir süre kullanan bir örnek gördüm! Executor.isTerminated ama bu çok zarif görünmüyor. Her işçi için bir geri arama özelliği uyguladım ve çalışan sayımı artırdım.
Ayı

5

Hiçbir görevin aynı anda çalışmadığından emin olmak istiyorsanız, bir SingleThreadedExecutor kullanın . Görevler gönderildikleri sıraya göre işlenecektir. Görevleri tutmanıza bile gerek yok, sadece exec'ye gönderin.


2

CallbackKullanarak mekanizma uygulamak için basit kodExecutorService

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

çıktı:

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

Önemli notlar:

  1. Eğer FIFO sırayla sırayla işlem görevleri istiyorsanız, yerini newFixedThreadPool(5) ilenewFixedThreadPool(1)
  2. callbackÖnceki görevin sonucunu analiz ettikten sonra bir sonraki görevi işlemek istiyorsanız , aşağıdaki satırın yorumunu kaldırmanız yeterlidir

    //System.out.println("future status:"+future.get()+":"+future.isDone());
  3. Şunlardan biriyle değiştirebilirsiniz newFixedThreadPool():

    Executors.newCachedThreadPool()
    Executors.newWorkStealingPool()
    ThreadPoolExecutor

    kullanım durumunuza bağlı olarak.

  4. Geri arama yöntemini eşzamansız olarak işlemek istiyorsanız

    a. Paylaşılan bir ExecutorService or ThreadPoolExecutorÇalışılabilir görev'e aktarma

    b. CallableYönteminizi Callable/Runnablegöreve dönüştürün

    c. Geri arama görevini ExecutorService or ThreadPoolExecutor


1

Sadece yardımcı olan Matt'in cevabına eklemek için, geri arama kullanımını göstermek için daha etli bir örnek.

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

Çıktı:

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

1

Bir Callable uygulaması kullanabilirsiniz.

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 çok basit bir şey

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

ve şimdi ana sınıf şöyle görünecek

ExecutorService ex = Executors.newFixedThreadPool(2);

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

1

Bu, Pache'nin Guava'ları kullanarak verdiği cevabın bir uzantısıdır ListenableFuture.

Özellikle, Futures.transform()dönüşler ListenableFuturezaman uyumsuz çağrıları zincirlemek için kullanılabilir. Futures.addCallback()döner void, bu yüzden zincirleme için kullanılamaz, ancak zaman uyumsuz bir tamamlamada başarı / başarısızlık için iyidir.

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

NOT: Zaman uyumsuz görevleri zincirlemenin yanı sıra, Futures.transform()her görevi ayrı bir yürütücüde zamanlamanıza da izin verir (Bu örnekte gösterilmemiştir).


Bu oldukça hoş görünüyor.
Kaiser
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.