如何在超时开始之前正确阻止线程?

时间:2014-07-05 13:31:20

标签: java threadpool

我想并行运行几个任务,直到经过一定的时间。让我们假设这些线程是CPU密集的和/或可能无限期地阻塞。超时后,线程应立即中断,主线程应继续执行,无论未完成或仍在运行的任务。

我已经看到了很多问题,并且答案总是相似的,通常按照"为任务创建线程池,启动它,在超时时加入#34;

问题出在" start"和"加入"部分。一旦允许池运行,它就可以获取CPU,并且在我将其恢复之前甚至不会启动超时。

我已经尝试过Executor.invokeAll,发现它并没有完全满足要求。例如:

    long dt = System.nanoTime ();
    ExecutorService pool = Executors.newFixedThreadPool (4);
    List <Callable <String>> list = new ArrayList <> ();
    for (int i = 0; i < 10; i++) {
        list.add (new Callable <String> () {
            @Override
            public String call () throws Exception {
                while (true) {
                }
            }
        });
    }
    System.out.println ("Start at " + (System.nanoTime () - dt) / 1000000 + "ms");
    try {
        pool.invokeAll (list, 3000, TimeUnit.MILLISECONDS);
    }
    catch (InterruptedException e) {
    }
    System.out.println ("End at " + (System.nanoTime () - dt) / 1000000 + "ms");
  

从1ms开始   结束于3028ms

这个(延迟27毫秒)可能看起来不太糟糕,但无限循环很容易被打破 - 实际程序的体验容易十倍。我的期望是即使在负载很重的情况下也能以非常高的精度满足超时请求(我正在考虑硬件中断,这应该总是工作)。

这是我的特定程序中的主要痛苦,因为它需要相当准确地注意某些超时(例如,如果可能的话,大约100毫秒更好)。但是,启动池通常需要400毫秒,直到我得到控制权,超过截止日期。

我有点困惑为什么几乎从未提及这个问题。我见过的大部分答案肯定都会受此影响。我想这通常是可以接受的,但就我而言,它不是。

是否有一个干净且经过测试的方法来解决这个问题?

已编辑添加:

我的程序涉及GC,即使不是大规模的。出于测试目的,我重写了上面的例子,发现给出的结果非常不一致,但平均比之前明显更差。

    long dt = System.nanoTime ();
    ExecutorService pool = Executors.newFixedThreadPool (40);
    List <Callable <String>> list = new ArrayList <> ();
    for (int i = 0; i < 10; i++) {
        list.add (new Callable <String> () {
            @Override
            public String call () throws Exception {
                String s = "";
                while (true) {
                    s += Long.toString (System.nanoTime ());
                    if (s.length () > 1000000) {
                        s = "";
                    }
                }
            }
        });
    }
    System.out.println ("Start at " + (System.nanoTime () - dt) / 1000000 + "ms");
    try {
        pool.invokeAll (list, 1000, TimeUnit.MILLISECONDS);
    }
    catch (InterruptedException e) {
    }
    System.out.println ("End at " + (System.nanoTime () - dt) / 1000000 + "ms");
  

从1ms开始   结束于1189ms

2 个答案:

答案 0 :(得分:1)

invokeAll应该可以正常工作。但是,编写任务以正确响应中断至关重要。捕获InterruptedException时,它们应立即退出。如果你的代码正在捕获IOException,那么每个这样的catch-block都应该有类似的东西:

} catch (InterruptedIOException e) {
    logger.log(Level.FINE, "Interrupted; exiting", e);
    return;
}

如果您使用Channels,则需要以同样的方式处理ClosedByInterruptException

如果执行无法捕获上述异常的耗时操作,则需要定期检查Thread.interrupted()。显然,更频繁地检查更好,尽管会有一个收益递减点。 (意思是,在任务中的每个语句之后检查它可能没用。)

if (Thread.interrupted()) {
    logger.fine("Interrupted; exiting");
    return;
}

在你的示例代码中,你的Callable根本没有检查中断状态,所以我的猜测是从不退出。中断实际上不会停止线程;它只是告诉线程它应该按照自己的条件终止自己。

答案 1 :(得分:0)

使用VM选项-XX:+PrintGCDetails,我发现GC运行得很少,但是时间延迟比预期的要大得多。这种延迟恰好恰逢我所经历的尖峰。

对观察到的行为的一种平凡和悲伤的解释。