【问题标题】:Future.cancel() followed by Future.get() kills my threadFuture.cancel() 后跟 Future.get() 杀死我的线程
【发布时间】:2019-06-16 05:37:27
【问题描述】:

我想使用 Executor 接口(使用 Callable)来启动一个 Thread(我们称之为可调用线程),它将做使用阻塞方法的工作。 这意味着当主线程调用 Future.cancel(true)(调用 Thread.interrupt() 时,可调用线程可以抛出 InterruptedException >)。

我还希望我的可调用线程在代码的取消部分中使用其他阻塞方法被中断时正确终止

在实现这一点时,我遇到了以下行为:当我调用 Future.cancel(true) 方法时,如果主线程立即等待,则可正确通知可调用线程中断BUT对于使用 Future.get() 来终止它,可调用线程在调用任何阻塞方法时都会被杀死

下面的JUnit 5 sn-p 说明了这个问题。 如果主线程在 cancel()get() 调用之间没有休眠,我们可以轻松地重现它。 如果我们睡了一会儿但还不够,我们可以看到可调用线程完成了一半的取消工作。 如果我们睡眠充足,可调用线程会正确完成其取消工作。

注意 1:我检查了可调用线程的中断状态:它正确设置了一次且仅一次,如预期的那样。

注意2:在中断后(传入取消代码时)逐步调试我的可调用线程时,在进入阻塞方法(没有 InterruptedException 似乎被抛出)。

    @Test
    public void testCallable() {

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        System.out.println("Main thread: Submitting callable...");
        final Future<Void> future = executorService.submit(() -> {

            boolean interrupted = Thread.interrupted();

            while (!interrupted) {
                System.out.println("Callable thread: working...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println("Callable thread: Interrupted while sleeping, starting cancellation...");
                    Thread.currentThread().interrupt();
                }
                interrupted = Thread.interrupted();
            }

            final int steps = 5;
            for (int i=0; i<steps; ++i) {
                System.out.println(String.format("Callable thread: Cancelling (step %d/%d)...", i+1, steps));
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Assertions.fail("Callable thread: Should not be interrupted!");
                }
            }

            return null;
        });

        final int mainThreadSleepBeforeCancelMs = 2000;
        System.out.println(String.format("Main thread: Callable submitted, sleeping %d ms...", mainThreadSleepBeforeCancelMs));

        try {
            Thread.sleep(mainThreadSleepBeforeCancelMs);
        } catch (InterruptedException e) {
            Assertions.fail("Main thread: interrupted while sleeping.");
        }

        System.out.println("Main thread: Cancelling callable...");
        future.cancel(true);
        System.out.println("Main thread: Cancelable just cancelled.");

        // Waiting "manually" helps to test error cases:
        // - Setting to 0 (no wait) will prevent the callable thread to correctly terminate;
        // - Setting to 500 will prevent the callable thread to correctly terminate (but some cancel process is done);
        // - Setting to 1500 will let the callable thread to correctly terminate.
        final int mainThreadSleepBeforeGetMs = 0;
        try {
            Thread.sleep(mainThreadSleepBeforeGetMs);
        } catch (InterruptedException e) {
            Assertions.fail("Main thread: interrupted while sleeping.");
        }

        System.out.println("Main thread: calling future.get()...");
        try {
            future.get();
        } catch (InterruptedException e) {
            System.out.println("Main thread: Future.get() interrupted: Error.");
        } catch (ExecutionException e) {
            System.out.println("Main thread: Future.get() threw an ExecutionException: Error.");
        } catch (CancellationException e) {
            System.out.println("Main thread: Future.get() threw an CancellationException: OK.");
        }

        executorService.shutdown();
    }

【问题讨论】:

    标签: java multithreading java-8 executorservice


    【解决方案1】:

    当您在取消的Future 上调用get() 时,您将得到一个CancellationException,因此不会等待Callable 的代码执行其清理。然后,您只是返回,当 JUnit 确定测试已完成时,观察到的线程被杀死的行为似乎是 JUnit 清理的一部分。

    为了等待完全清理,将最后一行从

    executorService.shutdown();
    

    executorService.shutdown();
    executorService.awaitTermination(1, TimeUnit.DAYS);
    

    请注意,在方法的 throws 子句中声明意外异常比使用调用 Assertions.failcatch 子句使测试代码混乱更简单。无论如何,JUnit 都会将此类异常报告为失败。

    然后,您可以删除整个sleep 代码。

    可能值得将ExecutorService 管理放入@Before/@After 甚至@BeforeClass/@AfterClass 方法中,以保持测试方法不受此影响,专注于实际测试。¹


    ¹ 这些是 JUnit 4 的名称。 IIRC,JUnit 5 名称分别类似于 @BeforeEach/@AfterEach@BeforeAll/@AfterAll

    【讨论】:

    • 感谢您的回复非常中肯。你能解释一下为什么我立即得到一个 CancellationException 而可调用线程还没有终止吗?这似乎与 Future.get() 手册不符:如有必要等待计算完成,然后检索其结果。
    • 好吧,当future被取消时,它的结果是canceled状态,所以没有必要进一步等待来提供那个结果。或者,换句话说,在那之后你的可调用对象可能会做的任何事情都不会改变这个结果。未来的焦点实际上是结果,而不是可能在后台运行的代码。 (对于较新的CompletableFuture 更是如此,它甚至不知道可能/最终会提供结果的代码)。
    猜你喜欢
    • 1970-01-01
    • 2019-12-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-25
    • 2014-10-24
    相关资源
    最近更新 更多