1. 개요
이 기사에서는 특정 시간이 지나면 장기 실행을 종료하는 방법을 알아봅니다. 이 문제에 대한 다양한 솔루션을 살펴보겠습니다. 또한 그들의 함정 중 일부를 다룰 것입니다.
2. 루프 사용
전자 상거래 애플리케이션의 제품 항목에 대한 일부 세부 정보와 같이 루프에서 여러 항목을 처리하고 있지만 모든 항목을 완료할 필요는 없을 수 있다고 상상해 보십시오.
실제로 특정 시간까지만 처리하고 그 이후에는 실행을 중지하고 해당 시간까지 List이 처리한 모든 것을 표시하려고 합니다.
간단한 예를 살펴보겠습니다.
long start = System.currentTimeMillis();
long end = start + 30 * 1000;
while (System.currentTimeMillis() < end) {
// Some expensive operation on the item.
}
여기에서 시간이 30초 제한을 초과하면 루프가 중단됩니다. 위의 솔루션에는 몇 가지 주목할만한 사항이 있습니다.
- 낮은 정확도: 루프가 지정된 시간 제한보다 더 오래 실행될 수 있습니다 . 이는 각 반복에 걸리는 시간에 따라 달라집니다. 예를 들어 각 반복이 최대 7초가 걸릴 수 있는 경우 총 시간은 최대 35초가 될 수 있으며 이는 원하는 시간 제한인 30초보다 약 17% 더 깁니다.
- 차단: 메인 스레드에서 이러한 처리는 오랜 시간 동안 차단하므로 좋은 생각이 아닐 수 있습니다 . 대신, 이러한 작업은 기본 스레드에서 분리되어야 합니다.
다음 섹션에서는 인터럽트 기반 접근 방식이 이러한 제한을 제거하는 방법에 대해 설명합니다.
3. 인터럽트 메커니즘 사용
여기에서는 별도의 스레드를 사용하여 장기 실행 작업을 수행합니다. 주 스레드는 시간 초과 시 작업자 스레드에 인터럽트 신호를 보냅니다.
작업자 스레드가 아직 살아 있으면 신호를 포착하고 실행을 중지합니다. 작업자가 제한 시간 전에 완료되면 작업자 스레드에 영향을 주지 않습니다.
작업자 스레드를 살펴보겠습니다.
class LongRunningTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < Long.MAX_VALUE; i++) {
if(Thread.interrupted()) {
return;
}
}
}
}
여기에서 Long.MAX_VALUE 를 통한 for 루프 는 장기 실행 작업을 시뮬레이트합니다. 이 대신 다른 작업이 있을 수 있습니다. 모든 작업이 인터럽트 가능한 것은 아니므로 인터럽트 플래그 를 확인하는 것이 중요합니다 . 따라서 이러한 경우 플래그를 수동으로 확인해야 합니다.
또한 모든 반복에서 이 플래그를 확인하여 최대 한 반복의 지연 시간 내에 스레드가 실행을 중지하도록 해야 합니다.
다음으로 인터럽트 신호를 보내는 세 가지 다른 메커니즘을 다룰 것입니다.
3.1. 타이머 사용
또는 TimerTask 를 생성하여 시간 초과 시 작업자 스레드를 중단 할 수 있습니다.
class TimeOutTask extends TimerTask {
private Thread thread;
private Timer timer;
public TimeOutTask(Thread thread, Timer timer) {
this.thread = thread;
this.timer = timer;
}
@Override
public void run() {
if(thread != null && thread.isAlive()) {
thread.interrupt();
timer.cancel();
}
}
}
여기에서 생성 시 작업자 스레드를 사용 하는 TimerTask 를 정의했습니다. run 메서드 호출 시 작업자 스레드 를 중단합니다 . 타이머 는 3초 지연 후 TimerTask 를 트리거합니다 .
Thread thread = new Thread(new LongRunningTask());
thread.start();
Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 3000);
3.2. Future#get 메서드 사용
Timer 를 사용하는 대신 Future 의 get 메소드를 사용할 수도 있습니다 .
ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new LongRunningTask());
try {
future.get(7, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
} catch (Exception e) {
// handle other exceptions
} finally {
executor.shutdownNow();
}
여기에서 우리는 ExecutorService 를 사용하여 Future 인스턴스를 반환하는 작업자 스레드를 제출했습니다. 이 스레드 의 get 메서드는 지정된 시간까지 기본 스레드를 차단합니다. 지정된 시간 초과 후 TimeoutException 이 발생합니다. catch 블록 에서 우리는 Future 개체 에 대한 취소 메서드를 호출하여 작업자 스레드를 중단 합니다.
이전 접근 방식에 비해 이 접근 방식의 주요 이점은 풀을 사용하여 스레드를 관리하는 반면 타이머 는 단일 스레드(풀 없음)만 사용한다는 것 입니다.
3.3. ScheduledExcecutorServicevice 사용
ScheduledExecutorService 를 사용하여 작업을 중단 할 수도 있습니다 . 이 클래스는 ExecutorService 의 확장 이며 실행 일정을 처리하는 여러 메서드를 추가하여 동일한 기능을 제공합니다. 설정된 시간 단위의 특정 지연 후에 주어진 작업을 실행할 수 있습니다.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
Future future = executor.submit(new LongRunningTask());
Runnable cancelTask = () -> future.cancel(true);
executor.schedule(cancelTask, 3000, TimeUnit.MILLISECONDS);
executor.shutdown();
여기에서는 newScheduledThreadPool 메서드를 사용하여 크기 2의 예약된 스레드 풀을 만들었습니다 . ScheduledExecutorService# 일정 방법은 Runnable , 지연 값 및 지연 단위를 사용합니다.
위의 프로그램은 제출 시점으로부터 3초 후에 작업이 실행되도록 예약합니다. 이 작업은 원래의 장기 실행 작업을 취소합니다.
이전 접근 방식과 달리 Future#get 메서드 를 호출하여 메인 스레드를 차단하지 않는다는 점에 유의하세요 . 따라서 위에서 언급한 모든 접근 방식 중에서 가장 선호되는 접근 방식 입니다.
4. 보증이 있습니까?
특정 시간 후에 실행이 중지된다는 보장은 없습니다 . 주된 이유는 모든 차단 방법이 인터럽트 가능한 것은 아니기 때문입니다. 실제로 인터럽트가 가능한 잘 정의된 메서드는 몇 가지에 불과합니다. 따라서 스레드가 중단되고 플래그가 설정되면 이러한 중단 가능한 메서드 중 하나에 도달할 때까지 아무 일도 일어나지 않습니다 .
예를 들어 읽기 및 쓰기 메서드는 InterruptibleChannel 로 생성된 스트림에서 호출되는 경우에만 인터럽트할 수 있습니다. BufferedReader 는 InterruptibleChannel 이 아닙니다 . 따라서 스레드가 이를 사용하여 파일을 읽는 경우 읽기 메서드 에서 차단된 이 스레드에서 인터럽트() 를 호출해도 아무런 효과가 없습니다.
그러나 루프에서 모든 읽기 후에 인터럽트 플래그를 명시적으로 확인할 수 있습니다. 이것은 약간의 지연으로 스레드를 중지하는 합리적인 보증을 제공합니다. 그러나 이것은 읽기 작업에 얼마나 많은 시간이 소요될 수 있는지 모르기 때문에 엄격한 시간 후에 스레드를 중지하는 것을 보장하지 않습니다.
반면에 Object 클래스의 wait 메소드 는 인터럽트 가능합니다. 따라서 대기 메서드에서 차단된 스레드 는 인터럽트 플래그가 설정된 후 즉시 InterruptedException 을 throw합니다.
메서드 서명에서 throws InterruptedException 을 찾아 차단 메서드를 식별할 수 있습니다 .
한 가지 중요한 조언은 더 이상 사용되지 않는 Thread.stop() 메서드를 사용하지 않는 것입니다. 스레드를 중지하면 잠긴 모든 모니터의 잠금이 해제됩니다. 이는 스택 위로 전파 되는 ThreadDeath 예외 때문에 발생합니다.
이전에 이러한 모니터에 의해 보호된 개체가 일관성 없는 상태인 경우 일관성 없는 개체가 다른 스레드에 표시됩니다. 이로 인해 감지하고 추론하기 매우 어려운 랜덤의 동작이 발생할 수 있습니다.
5. 중단을 위한 설계
이전 섹션에서 가능한 한 빨리 실행을 중지하기 위해 인터럽트 가능한 메서드를 갖는 것이 중요하다는 점을 강조했습니다. 따라서 우리의 코드는 설계 관점에서 이러한 기대치를 고려해야 합니다.
실행해야 할 장기 실행 작업이 있고 지정된 것보다 더 많은 시간이 걸리지 않도록 해야 한다고 상상해 보십시오. 또한 작업을 개별 단계로 나눌 수 있다고 가정합니다.
작업 단계에 대한 클래스를 생성해 보겠습니다.
class Step {
private static int MAX = Integer.MAX_VALUE/2;
int number;
public Step(int number) {
this.number = number;
}
public void perform() throws InterruptedException {
Random rnd = new Random();
int target = rnd.nextInt(MAX);
while (rnd.nextInt(MAX) != target) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
}
}
여기에서 Step#perform 메서드는 각 반복에서 플래그를 요청하면서 목표 랜덤의 정수를 찾으려고 시도합니다. 플래그가 활성화되면 메서드에서 InterruptedException 이 발생합니다.
이제 모든 단계를 수행할 작업을 정의해 보겠습니다.
public class SteppedTask implements Runnable {
private List<Step> steps;
public SteppedTask(List<Step> steps) {
this.steps = steps;
}
@Override
public void run() {
for (Step step : steps) {
try {
step.perform();
} catch (InterruptedException e) {
// handle interruption exception
return;
}
}
}
}
여기서 SteppedTask 에는 실행할 단계 List이 있습니다. for 루프는 각 단계를 수행 하고 작업이 발생할 때 작업을 중지하기 위해 InterruptedException 을 처리합니다.
마지막으로 인터럽트 가능한 작업을 사용하는 예를 살펴보겠습니다.
List<Step> steps = Stream.of(
new Step(1),
new Step(2),
new Step(3),
new Step(4))
.collect(Collectors.toList());
Thread thread = new Thread(new SteppedTask(steps));
thread.start();
Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 10000);
먼저 4단계 로 SteppedTask 를 만듭니다. 둘째, 스레드를 사용하여 작업을 실행합니다. 마지막으로 타이머와 시간 초과 작업을 사용하여 10초 후에 스레드를 중단합니다.
이 디자인을 사용하면 모든 단계를 실행하는 동안 장기 실행 작업을 중단할 수 있습니다. 이전에 본 것처럼 단점은 지정된 정확한 시간에 중지된다는 보장이 없지만 확실히 중단할 수 없는 작업보다 낫습니다.
6. 결론
이 예제에서는 각각의 장단점과 함께 주어진 시간 후에 실행을 중지하는 다양한 기술을 배웠습니다. 전체 소스 코드는 GitHub 에서 찾을 수 있습니다 .