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

Це "час початку" тайм-ауту під час подання? Або час, коли завдання починається виконувати?
Тім Бендер

Гарне питання. Коли він починає виконуватися. Імовірно за допомогою protected void beforeExecute(Thread t, Runnable r)гачка.
Едвард Дейл,

@ scompt.com ви все ще використовуєте це рішення чи воно було замінено
Пол Тейлор

@PaulTaylor Робота, де я впровадив це рішення, була замінена. :-)
Едвард Дейл

Мені потрібно саме це, крім а) Мені потрібна моя основна служба планувальника, щоб бути пулом потоків з єдиним потоком служби, оскільки мої завдання потрібно виконувати строго одночасно і б) Мені потрібно мати можливість вказати тривалість очікування для кожного завдання в час подання завдання. Я намагався використовувати це як вихідну точку, але розширюючи ScheduledThreadPoolExecutor, але я не бачу способу отримати вказану тривалість тайм-ауту, яка повинна бути вказана під час подання завдання до методу beforeExecute. Будь-які пропозиції з вдячністю вдячні!
Michael Ellis

Відповіді:


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 секунд, а потім скасує (тобто перерве) це конкретне завдання.


13
Цікава ідея, але що робити, якщо завдання закінчується до тайм-ауту (який він зазвичай виконує)? Я вважаю за краще, щоб безліч завдань очищення не чекало запуску, аби дізнатись, що призначене їм завдання вже виконане. Потрібно мати ще один потік, який відстежує майбутнє, коли вони закінчують, щоб видалити свої завдання з очищення.
Едвард Дейл,

3
Виконавець призначить це скасування лише один раз. Якщо завдання виконано, скасування не виконується, і робота продовжується незмінною. Потрібно лише один додатковий схематичний потік для скасування завдань і один потік для їх запуску. Ви можете мати двох виконавців, одного для подання ваших основних завдань, а одного для їх скасування.
Джон Вінт,

3
Це правда, але що робити, якщо час очікування становить 5 годин, і за цей час виконується 10 тис. Завдань. Я хотів би уникнути того, щоб усі ці операції лежали, забираючи пам'ять і викликаючи перемикання контексту.
Едвард Дейл,

1
@Scompt Не обов'язково. Було б 10 тис. Викликів future.cancel (), однак якщо майбутнє завершено, то скасування швидко вийде і не зробить ніякої непотрібної роботи. Якщо ви не хочете 10 тис. Додаткових викликів скасування, це може не спрацювати, але обсяг роботи, виконаної після виконання завдання, дуже малий.
Джон Вінт,

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

Програма вичерпує доступну пам'ять, хоча вона чекає завершення ікру Runnable.

Я хоч про це деякий час, але, на жаль, я не міг знайти хорошого рішення.

EDIT: Я дізнався, що ця проблема була зареєстрована як помилка 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());

ListeningExecutorServiceце функція прослуховування проекту ExecutorServicegoogle guava ( com.google.guava ))


2
Дякую, що вказали invokeAll. Це працює дуже добре. Лише слово обережності для тих, хто замислюється про використання цього: хоча і invokeAllповертає список Futureоб’єктів, насправді це видається операцією блокування.
mxro


1

Здається, проблема полягає не в помилці JDK 6602600 (вона була вирішена на 22.05.2010), а в неправильному виклику сну (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);
//                } 

Він працює коректно з кількістю зовнішнього лічильника до 150 000 000 перевірених кіл.


1

За допомогою відповіді John W я створив реалізацію, яка правильно розпочинає час очікування, коли завдання починає своє виконання. Я навіть пишу для цього юніт-тест :)

Однак це не відповідає моїм потребам, оскільки деякі операції вводу-виводу не переривають, коли Future.cancel()викликається (тобто коли Thread.interrupt()викликається). Деякі приклади операції введення - виведення , які не можуть бути перервані , коли Thread.interrupt()викликається є Socket.connectі Socket.read(і я підозрюю , що більшість операцій введення - виведення , реалізованого в java.io). Усі операції вводу-виводу в java.nioповинні бути переривними при Thread.interrupt()виклику. Наприклад, це стосується SocketChannel.openі SocketChannel.read.

У будь-якому випадку, якщо комусь цікаво, я створив суть для виконавця пулу потоків, що дозволяє таймауту завдань (якщо вони використовують переривні операції ...): https://gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf


Цікавий код, я втягнув його в свою систему і цікаво, якщо у вас є кілька прикладів того, які операції вводу-виводу не будуть перериватися, тому я бачу, чи це вплине на мою систему. Дякую!
Дункан Кребс,

@DuncanKrebs Я деталізував свою відповідь на прикладі неперериваного вводу-виводу: Socket.connect іSocket.read
amanteaux

myThread.interrupted()не є правильним методом переривання, оскільки він очищає прапор переривання. Використовуйте myThread.interrupt()замість цього, і це слід із сокетами
DanielCuadra

@DanielCuadra: Дякую, схоже, я допустив помилку при друкарській помилці, оскільки Thread.interrupted()не дозволяє переривати потік. Однак Thread.interrupt()не перебиваєjava.io операції, він працює лише над java.nioопераціями.
amanteaux

Я використовував interrupt()багато років, і це завжди переривало операції java.io (а також інші методи блокування, такі як сплячий потік, jdbc-з'єднання, blockingqueue take тощо). Можливо, ви знайшли клас баггі чи якусь JVM, яка має помилки
DanielCuadra,

0

А як щодо цієї альтернативної ідеї:

  • двоє мають двох виконавців:
    • один для:
      • подання завдання, не дбаючи про час очікування завдання
      • додавання результату майбутнього та часу, коли воно повинно закінчитися, до внутрішньої структури
    • одна для виконання внутрішньої роботи, яка перевіряє внутрішню структуру, якщо деякі завдання таймаутуються і чи їх потрібно скасувати.

Невеликий зразок тут:

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

Ви можете обмежити використання потоків із планувальника, а також встановити час очікування для завдання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.