시간 초과 후 작업을 중단하는 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

좋은 질문. 실행을 시작할 때. 아마도 protected void beforeExecute(Thread t, Runnable r)후크를 사용하는 것 같습니다 .
Edward Dale

@ scompt.com 당신은 여전히이 솔루션을 사용되거나 대체 된 한
폴 테일러

@PaulTaylor이 솔루션을 구현 한 작업이 대체되었습니다. :-)
Edward Dale

a) 내 작업을 엄격하게 동시에 실행해야하므로 기본 스케줄러 서비스가 단일 서비스 스레드가있는 스레드 풀이되어야합니다 .b) 작업이 제출 된 시간입니다. 나는 이것을 시작점으로 사용했지만 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
흥미로운 아이디어이지만 작업이 시간 초과 (일반적으로 수행됨) 전에 완료되면 어떻게됩니까? 차라리 할당 된 작업이 이미 완료되었는지 확인하기 위해 실행되기를 기다리는 많은 정리 작업이 필요하지 않습니다. 정리 작업을 제거하기 위해 완료하는 동안 Futures를 모니터링하는 또 다른 스레드가 필요합니다.
Edward Dale

3
실행자는이 취소를 한 번만 예약합니다. 작업이 완료되면 취소는 작업 없음이며 작업은 변경되지 않고 계속됩니다. 작업을 취소하는 데 하나의 추가 스레드와 작업을 실행하는 데 하나의 스레드 만 있으면됩니다. 두 개의 실행자가있을 수 있습니다. 하나는 주요 작업을 제출하고 다른 하나는 취소 할 수 있습니다.
John Vint

3
사실이지만 시간 제한이 5 시간이고 그 시간에 10,000 개의 작업이 실행되면 어떻게 될까요? 메모리를 차지하고 컨텍스트 전환을 유발하는 모든 no-ops를 피하고 싶습니다.
Edward Dale

1
@Scompt 반드시 그런 것은 아닙니다. 10k future.cancel () 호출이있을 수 있지만, future.cancel () 호출이 완료되면 취소는 빠른 경로로 빠져 나가고 비정상적인 작업을 수행하지 않습니다. 10k 추가 취소 호출을 원하지 않는 경우 작동하지 않을 수 있지만 작업이 완료 될 때 수행되는 작업량은 매우 적습니다.
John Vint

6
@John W .: 방금 구현에서 또 다른 문제를 깨달았습니다. 앞서 언급했듯이 작업이 실행을 시작할 때 시작하려면 시간 제한이 필요합니다. 그렇게하는 유일한 방법은 beforeExecute후크 를 사용하는 것 입니다.
Edward Dale

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에 대한 시간 제한을 지정할 수 있습니다. 이 질문에 대한 내 대답의 예를보십시오.

자바 네이티브 프로세스 시간 초과


1
java.util.concurrent클래스를 사용하여이 작업을 수행하는 방법이 몇 가지 있다는 것을 알고 있지만 ExecutorService구현을 찾고 있습니다.
Edward Dale

1
ExecutorService가 클라이언트 코드에서 시간 초과가 추가된다는 사실을 숨기도록하려면 실행하기 전에 FutureTask로 전달 된 모든 실행 파일을 래핑하는 자체 ExecutorService를 구현할 수 있습니다.
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());

ListeningExecutorServiceExecutorServiceat google guava project ( com.google.guava ) 의 듣기 기능입니다 .


2
지적 해 주셔서 감사합니다 invokeAll. 아주 잘 작동합니다. 이것을 사용하는 것에 대해 생각하는 모든 사람들에게주의 할 점 : 객체 invokeAll목록을 반환 하지만 Future실제로는 차단 작업 인 것 같습니다.
mxro


1

문제는 JDK 버그 6602600 (2010-05-22에서 해결됨)이 아니라 원 안에 sleep (10)을 잘못 호출 한 것 같습니다. 참고로, 메인 스레드는 외부 원의 모든 분기에서 SLEEP (0)을 호출하여 자신의 작업을 실현하기 위해 다른 스레드에 직접 CHANCE를 제공해야합니다. Thread.sleep (0) 대신 Thread.yield ()를 사용하는 것이 더 좋습니다.

이전 문제 코드의 결과 수정 된 부분은 다음과 같습니다.

.......................
........................
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 답변을 사용하여 작업이 실행을 시작할 때 시간 초과를 올바르게 시작하는 구현을 만들었습니다. 나는 그것에 대한 단위 테스트도 작성합니다 :)

그러나 일부 IO 작업 Future.cancel()이 호출 될 때 (즉, 호출 될 때 ) 중단되지 않기 때문에 내 요구에 적합하지 않습니다 Thread.interrupt(). Thread.interrupt()호출 될 때 중단되지 않을 수있는 IO 작업의 몇 가지 예 는 Socket.connectSocket.read(그리고에서 구현 된 IO 작업의 대부분을 의심합니다 java.io). 의 모든 IO 작업 은이 호출 java.nio될 때 중단 될 수 있어야합니다 Thread.interrupt(). 예를 들어, SocketChannel.open및 의 경우입니다 SocketChannel.read.

어쨌든 관심이 있다면 작업 시간 초과를 허용하는 스레드 풀 실행기에 대한 요점을 만들었습니다 (중단 가능한 작업을 사용하는 경우 ...) : https://gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf


흥미로운 코드는 내 시스템으로 가져 와서 어떤 종류의 IO 작업이 중단되지 않는지에 대한 몇 가지 예가 있는지 궁금합니다. 그래서 내 시스템에 영향을 미칠지 확인할 수 있습니다. 감사!
Duncan Krebs

@DuncanKrebs 나는 중단 불가능한 IO의 예를 들어 내 대답을 자세히 설명했습니다. Socket.connect그리고Socket.read
amanteaux

myThread.interrupted()인터럽트 플래그를 지우므로 올바른 인터럽트 방법이 아닙니다. myThread.interrupt()대신 사용 하고 소켓과 함께 사용해야합니다.
DanielCuadra

@DanielCuadra : 감사합니다 Thread.interrupted(). 스레드를 중단 할 수없는 오타를 범한 것 같습니다 . 그러나 작업을 Thread.interrupt()중단하지 않고 java.io작업에서만 java.nio작동합니다.
amanteaux

나는 interrupt()수년 동안 사용해 왔으며 항상 java.io 작업 (뿐만 아니라 스레드 절전, jdbc 연결, 블로킹 큐 가져 오기 등과 같은 다른 차단 방법)을 중단했습니다. 버그가있는 클래스 또는 버그가있는 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;
  }

스케줄러에서 스레드 사용 수를 제한하고 작업에 시간 제한을 둘 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.