ExecutorService在超时后中断任务


93

我正在寻找可以提供超时的ExecutorService实现。如果提交给ExecutorService的任务花费的时间超过了超时时间,则这些任务将被中断。实现这样的野兽并不是一个困难的任务,但是我想知道是否有人知道现有的实现。

这是我根据以下一些讨论得出的。任何意见?

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

超时的“开始时间”是提交时间吗?还是任务开始执行的时间?
Tim Bender 2010年

好问题。开始执行时。大概使用了protected void beforeExecute(Thread t, Runnable r)钩子。
爱德华·戴尔

@ scompt.com你还在使用此解决方案或已将它所取代
保罗·泰勒

@PaulTaylor我实施此解决方案的工作已被取代。:-)
爱德华·戴尔

除了a)我需要主调度程序服务是一个具有单个服务线程的线程池外,因为我的任务必须严格同时执行,并且b)我需要能够为每个任务指定超时时间,任务提交的时间。我尝试以此为起点,但是扩展了ScheduledThreadPoolExecutor,但是我看不到一种方法,该方法可以将指定的超时时间(在任务提交时指定)通过beforeExecute方法获取。任何建议表示感谢!
Michael Ellis

Answers:


89

您可以为此使用ScheduledExecutorService。首先,您只提交一次即可立即开始,并保留创建的未来。之后,您可以提交新任务,该任务将在一段时间后取消保留的将来。

 ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); 
 final Future handler = executor.submit(new Callable(){ ... });
 executor.schedule(new Runnable(){
     public void run(){
         handler.cancel();
     }      
 }, 10000, TimeUnit.MILLISECONDS);

这将使您的处理程序(主要功能被中断)执行10秒钟,然后将取消(即中断)该特定任务。


12
有趣的想法,但是如果任务在超时之前完成(通常会完成)怎么办?我宁愿没有大量清理任务等待运行,只是发现分配的任务已经完成。在期货结束时,需要有另一个线程监视期货以删除其清理任务。
爱德华·戴尔

3
执行者只会安排一次取消。如果任务已完成,则取消是无操作,并且工作将继续不变。只需要安排一个额外的线程来取消任务,再运行一个线程即可。您可以有两名执行者,一名执行主任务,另一名取消主任务。
约翰·芬特

3
没错,但是如果超时是5个小时并且在那时间内执行了10k任务该怎么办。我想避免所有那些无操作的操作占用内存并引起上下文切换。
爱德华·戴尔

1
@Scompt不一定。将有1万个future.cancel()调用,但是,如果将来完成,则取消将快速退出,并且不会进行任何不必要的工作。如果您不希望额外进行10k的取消调用,那么这可能不起作用,但是当任务完成时完成的工作量很小。
约翰·温特

6
@John W .:我刚刚意识到您的实现还有另一个问题。如前所述,我需要在任务开始执行时开始超时。我认为唯一的方法就是使用beforeExecute钩子。
爱德华·戴尔

6

不幸的是,该解决方案是有缺陷的。这个问题ScheduledThreadPoolExecutor也有一个错误提示,也存在这个错误:取消已提交的任务不会完全释放与该任务相关的内存资源;仅在任务到期时才释放资源。

如果因此创建一个 TimeoutThreadPoolExecutor具有相当长的到期时间(典型用法)的,并以足够快的速度提交任务,那么即使任务实际成功完成,您也将最终占用内存。

您可以使用以下(非常粗糙的)测试程序查看问题:

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

程序将等待可用的Runnables完成,但会耗尽可用的内存。

我虽然花了一段时间,但不幸的是我无法提出一个好的解决方案。

编辑:我发现此问题被报告为JDK错误6602600,并且似乎已经得到解决。


4

将任务包装在FutureTask中,然后可以为FutureTask指定超时。看我对这个问题的回答中的例子,

java本机进程超时


1
我意识到使用java.util.concurrent类可以通过几种方法来实现,但是我正在寻找一种ExecutorService实现。
爱德华·戴尔

1
如果您要让ExecutorService隐藏从客户端代码中添加超时的事实,则可以实现自己的ExecutorService,在执行它们之前,它会使用FutureTask将传递给它的所有可运行对象包装起来。
erikprice

2

经过大量时间的调查,
最后,我使用的invokeAll方法ExecutorService解决了这个问题。
这将在任务运行时严格中断任务。
这是例子

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

亲是也可以提交ListenableFuture同一ExecutorService
只需稍微更改第一行代码即可。

ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());

ListeningExecutorServiceExecutorServicegoogle guava项目(com.google.guava)的听力功能


2
感谢您指出invokeAll。效果很好。对于任何考虑使用此功能的人来说,只有一点警告:尽管invokeAll返回Future对象列表,但实际上似乎是一个阻塞操作。
mxro


1

看来问题不在JDK错误6602600中(此问题已在2010-05-22中解决),而是在圈子中不正确地调用sleep(10)。另外请注意,主线程必须通过在外圈的每个分支中调用SLEEP(0)来直接给其他线程以机会来实现其任务。我认为最好使用Thread.yield()而不是Thread.sleep(0)

先前问题代码的结果校正部分是这样的:

.......................
........................
Thread.yield();         

if (i % 1000== 0) {
System.out.println(i + "/" + counter.get()+ "/"+service.toString());
}

//                
//                while (i > counter.get()) {
//                    Thread.sleep(10);
//                } 

它可以正常工作,并且外部计数器的数量最多为1.5亿个测试圈。


1

使用John W答案,我创建了一个在任务开始执行时正确开始超时的实现。我什至为此编写了单元测试:)

但是,它不符合我的需求,因为某些IO操作在Future.cancel()被调用时(即何时Thread.interrupt()被调用)不会中断。Thread.interrupt()调用时可能不会中断的IO操作的一些示例是Socket.connectSocket.read(而且我怀疑大部分IO操作是在中实现的java.io)。调用java.nio时,所有IO操作均应可中断Thread.interrupt()。例如,对于壳体SocketChannel.openSocketChannel.read

无论如何,如果有人感兴趣,我为线程池执行程序创建了要点,该任务允许任务超时(如果它们正在使用可中断的操作...):https : //gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf


有趣的代码,我将其拉入系统,并想知道是否有一些示例可以说明哪种IO操作不会中断,因此我可以查看它是否会影响系统。谢谢!
邓肯·克雷布斯

@DuncanKrebs我详细说明了我的答案与非中断IO的一个例子:Socket.connectSocket.read
amanteaux

myThread.interrupted()不是正确的中断方法,因为它会清除中断标志。myThread.interrupt()改为使用,并且应该与套接字一起使用
DanielCuadra

@DanielCuadra:谢谢,看来我犯了一个错字错误,因为Thread.interrupted()它无法中断线程。但是,Thread.interrupt()不会中断java.io操作,它仅适用于java.nio操作。
amanteaux

我已经使用interrupt()了很多年,它一直中断java.io操作(以及其他阻塞方法,例如线程睡眠,jdbc连接,阻塞队列获取等)。也许您发现了一个有错误的类或某些具有错误的JVM
DanielCuadra

0

那这个替代想法呢?

  • 两个有两个执行者:
    • 一个用于:
      • 提交任务,而不关心任务的超时
      • 添加Future结果以及结束内部结构的时间
    • 一种用于执行内部作业的程序,用于检查某些任务是否超时以及是否必须取消它们的内部结构。

小样本在这里:

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

0

检查是否适合您,

    public <T,S,K,V> ResponseObject<Collection<ResponseObject<T>>> runOnScheduler(ThreadPoolExecutor threadPoolExecutor,
      int parallelismLevel, TimeUnit timeUnit, int timeToCompleteEachTask, Collection<S> collection,
      Map<K,V> context, Task<T,S,K,V> someTask){
    if(threadPoolExecutor==null){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("threadPoolExecutor can not be null").build();
    }
    if(someTask==null){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("Task can not be null").build();
    }
    if(CollectionUtils.isEmpty(collection)){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("input collection can not be empty").build();
    }

    LinkedBlockingQueue<Callable<T>> callableLinkedBlockingQueue = new LinkedBlockingQueue<>(collection.size());
    collection.forEach(value -> {
      callableLinkedBlockingQueue.offer(()->someTask.perform(value,context)); //pass some values in callable. which can be anything.
    });
    LinkedBlockingQueue<Future<T>> futures = new LinkedBlockingQueue<>();

    int count = 0;

    while(count<parallelismLevel && count < callableLinkedBlockingQueue.size()){
      Future<T> f = threadPoolExecutor.submit(callableLinkedBlockingQueue.poll());
      futures.offer(f);
      count++;
    }

    Collection<ResponseObject<T>> responseCollection = new ArrayList<>();

    while(futures.size()>0){
      Future<T> future = futures.poll();
      ResponseObject<T> responseObject = null;
        try {
          T response = future.get(timeToCompleteEachTask, timeUnit);
          responseObject = ResponseObject.<T>builder().data(response).build();
        } catch (InterruptedException e) {
          future.cancel(true);
        } catch (ExecutionException e) {
          future.cancel(true);
        } catch (TimeoutException e) {
          future.cancel(true);
        } finally {
          if (Objects.nonNull(responseObject)) {
            responseCollection.add(responseObject);
          }
          futures.remove(future);//remove this
          Callable<T> callable = getRemainingCallables(callableLinkedBlockingQueue);
          if(null!=callable){
            Future<T> f = threadPoolExecutor.submit(callable);
            futures.add(f);
          }
        }

    }
    return ResponseObject.<Collection<ResponseObject<T>>>builder().data(responseCollection).build();
  }

  private <T> Callable<T> getRemainingCallables(LinkedBlockingQueue<Callable<T>> callableLinkedBlockingQueue){
    if(callableLinkedBlockingQueue.size()>0){
      return callableLinkedBlockingQueue.poll();
    }
    return null;
  }

您可以限制调度程序的线程使用数量,也可以限制任务超时。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.