Je recherche une implémentation ExecutorService pouvant être fournie avec un délai d'attente. Les tâches soumises à ExecutorService sont interrompues si leur exécution prend plus de temps que le délai imparti. Implémenter une telle bête n’est pas une tâche aussi difficile, mais je me demande si quelqu'un connaît une implémentation existante.
Voici ce que j'ai trouvé sur la base de certaines des discussions ci-dessous. Des commentaires?
import Java.util.List;
import Java.util.concurrent.*;
public class TimeoutThreadPoolExecutor extends ThreadPoolExecutor {
private final long timeout;
private final TimeUnit timeoutUnit;
private final ScheduledExecutorService timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentMap<Runnable, ScheduledFuture> runningTasks = new ConcurrentHashMap<Runnable, ScheduledFuture>();
public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, long timeout, TimeUnit timeoutUnit) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, long timeout, TimeUnit timeoutUnit) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler, long timeout, TimeUnit timeoutUnit) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler, long timeout, TimeUnit timeoutUnit) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
@Override
public void shutdown() {
timeoutExecutor.shutdown();
super.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
timeoutExecutor.shutdownNow();
return super.shutdownNow();
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
if(timeout > 0) {
final ScheduledFuture<?> scheduled = timeoutExecutor.schedule(new TimeoutTask(t), timeout, timeoutUnit);
runningTasks.put(r, scheduled);
}
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
ScheduledFuture timeoutTask = runningTasks.remove(r);
if(timeoutTask != null) {
timeoutTask.cancel(false);
}
}
class TimeoutTask implements Runnable {
private final Thread thread;
public TimeoutTask(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
thread.interrupt();
}
}
}
Vous pouvez utiliser un ScheduledExecutorService pour cela. D'abord, vous le soumettriez une seule fois pour commencer immédiatement et conserver le futur créé. Après cela, vous pouvez soumettre une nouvelle tâche qui annulerait le futur retenu après un certain temps.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
final Future handler = executor.submit(new Callable(){ ... });
executor.schedule(new Runnable(){
public void run(){
handler.cancel();
}
}, 10000, TimeUnit.MILLISECONDS);
Ceci exécutera votre gestionnaire (fonctionnalité principale à interrompre) pendant 10 secondes, puis annulera (c'est-à-dire interrompra) cette tâche spécifique.
Malheureusement, la solution est imparfaite. Il y a une sorte de bogue avec ScheduledThreadPoolExecutor
, également signalé dans cette question : annuler une tâche soumise ne libère pas complètement les ressources de mémoire associées à la tâche; les ressources ne sont libérées qu'à l'expiration de la tâche.
Par conséquent, si vous créez un TimeoutThreadPoolExecutor
avec un délai d’expiration assez long (utilisation typique) et soumettez les tâches assez rapidement, vous finissez par remplir la mémoire - même si les tâches se sont bien terminées.
Vous pouvez voir le problème avec le programme de test suivant (très grossier):
public static void main(String[] args) throws InterruptedException {
ExecutorService service = new TimeoutThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), 10, TimeUnit.MINUTES);
//ExecutorService service = Executors.newFixedThreadPool(1);
try {
final AtomicInteger counter = new AtomicInteger();
for (long i = 0; i < 10000000; i++) {
service.submit(new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
});
if (i % 10000 == 0) {
System.out.println(i + "/" + counter.get());
while (i > counter.get()) {
Thread.sleep(10);
}
}
}
} finally {
service.shutdown();
}
}
Le programme épuise la mémoire disponible, bien qu'il attende que le Runnable
s engendré soit terminé.
J'y ai réfléchi pendant un moment, mais malheureusement, je n'ai pas pu trouver de bonne solution.
EDIT: J'ai découvert que ce problème avait été signalé comme bogue JDK 66026 et semble avoir été corrigé très récemment.
Emballez la tâche dans FutureTask et vous pouvez spécifier un délai d'expiration pour FutureTask. Regardez l'exemple dans ma réponse à cette question,
Il semble que le problème ne se trouve pas dans le bogue JDK 6602600 (il a été résolu le 22/05/2010), mais dans un appel incorrect de sleep (10) en cercle. Remarque supplémentaire, le thread principal doit donner directement CHANCE aux autres threads pour réaliser leurs tâches en appelant SLEEP (0) dans CHAQUE branche du cercle extérieur. Il vaut mieux, je pense, utiliser Thread.yield () au lieu de Thread.sleep (0)
Le résultat corrigé dans le code du problème précédent est le suivant:
.......................
........................
Thread.yield();
if (i % 1000== 0) {
System.out.println(i + "/" + counter.get()+ "/"+service.toString());
}
//
// while (i > counter.get()) {
// Thread.sleep(10);
// }
Cela fonctionne correctement avec la quantité de compteur externe jusqu’à 150 000 000 de cercles testés.
Après une tonne de temps pour enquêter,
Enfin, j’utilise invokeAll
méthode de ExecutorService
pour résoudre ce problème.
Cela interrompra strictement la tâche en cours d'exécution.
Voici un exemple
ExecutorService executorService = Executors.newCachedThreadPool();
try {
List<Callable<Object>> callables = new ArrayList<>();
// Add your long time task (callable)
callables.add(new VaryLongTimeTask());
// Assign tasks for specific execution timeout (e.g. 2 sec)
List<Future<Object>> futures = executorService.invokeAll(callables, 2000, TimeUnit.MILLISECONDS);
for (Future<Object> future : futures) {
// Getting result
}
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.shutdown();
Le pro est que vous pouvez aussi soumettre ListenableFuture
en même temps ExecutorService
.
Modifiez légèrement la première ligne de code.
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListeningExecutorService
est la fonctionnalité d'écoute de ExecutorService
dans le projet google guava (com.google.guava))
Pourquoi ne pas utiliser la méthode ExecutorService.shutDownNow()
comme décrit dans http://docs.Oracle.com/javase/7/docs/api/Java/util/concurrent/ExecutorService.html ? Cela semble être la solution la plus simple.
En utilisant John W answer, j'ai créé une implémentation qui commence correctement le délai d'expiration lorsque la tâche commence son exécution. J'ai même écrit un test unitaire pour ça :)
Cependant, cela ne convient pas à mes besoins car certaines opérations IO ne s'interrompent pas lorsque Future.cancel()
est appelée (c'est-à-dire lorsque Thread.interrupted()
est appelée)). Quelques exemples of IO) Une opération qui ne peut pas être interrompue lorsque Thread.interrupted()
est appelée est Socket.connect
et Socket.read
(et je soupçonne que la plupart IO implémentée dans Java.io
). Toutes les opérations IO dans Java.nio
Devraient être interruptibles lorsque Thread.interrupted()
est appelée Par exemple, c'est le cas pour SocketChannel.open
Et SocketChannel.read
.
Quoi qu'il en soit, si quelqu'un est intéressé, j'ai créé un Gist pour un exécuteur de pool de threads qui autorise le dépassement de délai des tâches (s'ils utilisent des opérations interruptibles ...): https://Gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf
Qu'en est-il de cette idée alternative:
Petit échantillon est ici:
public class AlternativeExecutorService
{
private final CopyOnWriteArrayList<ListenableFutureTask> futureQueue = new CopyOnWriteArrayList();
private final ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1); // used for internal cleaning job
private final ListeningExecutorService threadExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5)); // used for
private ScheduledFuture scheduledFuture;
private static final long INTERNAL_JOB_CLEANUP_FREQUENCY = 1000L;
public AlternativeExecutorService()
{
scheduledFuture = scheduledExecutor.scheduleAtFixedRate(new TimeoutManagerJob(), 0, INTERNAL_JOB_CLEANUP_FREQUENCY, TimeUnit.MILLISECONDS);
}
public void pushTask(OwnTask task)
{
ListenableFuture<Void> future = threadExecutor.submit(task); // -> create your Callable
futureQueue.add(new ListenableFutureTask(future, task, getCurrentMillisecondsTime())); // -> store the time when the task should end
}
public void shutdownInternalScheduledExecutor()
{
scheduledFuture.cancel(true);
scheduledExecutor.shutdownNow();
}
long getCurrentMillisecondsTime()
{
return Calendar.getInstance().get(Calendar.MILLISECOND);
}
class ListenableFutureTask
{
private final ListenableFuture<Void> future;
private final OwnTask task;
private final long milliSecEndTime;
private ListenableFutureTask(ListenableFuture<Void> future, OwnTask task, long milliSecStartTime)
{
this.future = future;
this.task = task;
this.milliSecEndTime = milliSecStartTime + task.getTimeUnit().convert(task.getTimeoutDuration(), TimeUnit.MILLISECONDS);
}
ListenableFuture<Void> getFuture()
{
return future;
}
OwnTask getTask()
{
return task;
}
long getMilliSecEndTime()
{
return milliSecEndTime;
}
}
class TimeoutManagerJob implements Runnable
{
CopyOnWriteArrayList<ListenableFutureTask> getCopyOnWriteArrayList()
{
return futureQueue;
}
@Override
public void run()
{
long currentMileSecValue = getCurrentMillisecondsTime();
for (ListenableFutureTask futureTask : futureQueue)
{
consumeFuture(futureTask, currentMileSecValue);
}
}
private void consumeFuture(ListenableFutureTask futureTask, long currentMileSecValue)
{
ListenableFuture<Void> future = futureTask.getFuture();
boolean isTimeout = futureTask.getMilliSecEndTime() >= currentMileSecValue;
if (isTimeout)
{
if (!future.isDone())
{
future.cancel(true);
}
futureQueue.remove(futureTask);
}
}
}
class OwnTask implements Callable<Void>
{
private long timeoutDuration;
private TimeUnit timeUnit;
OwnTask(long timeoutDuration, TimeUnit timeUnit)
{
this.timeoutDuration = timeoutDuration;
this.timeUnit = timeUnit;
}
@Override
public Void call() throws Exception
{
// do logic
return null;
}
public long getTimeoutDuration()
{
return timeoutDuration;
}
public TimeUnit getTimeUnit()
{
return timeUnit;
}
}
}