タイムアウトを指定できる 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();
}
}
}
これには 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秒間実行され、その特定のタスクがキャンセル(中断)されます。
残念ながら、ソリューションには欠陥があります。 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
sが完了するまで待機しますが、使用可能なメモリを使い果たします。
私はしばらくこのことについて考えましたが、残念ながら良い解決策を思い付くことができませんでした。
編集:この問題は JDK bug 66026 として報告されており、ごく最近修正されたようです。
FutureTaskでタスクをラップすると、FutureTaskのタイムアウトを指定できます。この質問に対する私の答えの例を見てください。
問題はJDKバグ6602600(2010-05-22で解決済み)ではなく、不正なsleep(10)の呼び出しにあるようです。さらに、メインスレッドは、外側の円のすべてのブランチでSLEEP(0)を呼び出してタスクを実現するために、他のスレッドに直接チャンスを与える必要があることに注意してください。 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のテスト済みサークルまでの外部カウンターの量で正しく機能します。
膨大な時間をかけて調査した後、
最後に、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
は、GoogleグアバプロジェクトのExecutorService
のリスニング機能です(-com.google.guava))
http://docs.Oracle.com/javase/7/docs/api/Java/util/concurrent/ExecutorService.html で説明されているExecutorService.shutDownNow()
メソッドを使用してはどうですか。最も簡単な解決策のようです。
John Wの回答を使用して、タスクの実行開始時にタイムアウトを正しく開始する実装を作成しました。私もそれのためのユニットテストを書く:)
ただし、Future.cancel()
が呼び出されたときに(つまり、Thread.interrupted()
が呼び出されたときに)いくつかのIO操作が中断しないため、私のニーズには合いません。 Thread.interrupted()
が呼び出されたときに中断されない可能性のあるIO操作の例は、Socket.connect
およびSocket.read
です(ほとんどのIO Java.io
)で実装された操作。 Thread.interrupted()
が呼び出された場合、Java.nio
のすべてのIO操作は割り込み可能である必要があります。たとえば、SocketChannel.open
およびSocketChannel.read
の場合です。
とにかく誰かが興味を持っているなら、タスクをタイムアウトできるスレッドプールエグゼキューター用のGistを作成しました(割り込み可能な操作を使用している場合...): https://Gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf
この代替案はどうですか:
小さなサンプルはこちら:
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;
}
}
}