Future.cancel (), за которым следует Future.get (), убивает мой поток - PullRequest
0 голосов
/ 22 января 2019

Я хочу использовать интерфейс Executor (используя Callable ), чтобы запустить Thread (назовем его вызываемым Thread), который будет выполнять работу, которая использует методы блокировки. Это означает, что вызываемый поток может выдать InterruptedException , когда основной поток вызывает Future.cancel (true) (который вызывает Thread.interrupt () ).

Я также хочу, чтобы мой вызываемый поток правильно завершался при прерывании, ИСПОЛЬЗУЯ другие методы блокировки в части отмены кода.

При реализации этого я испытал следующее поведение: Когда я вызываю Future.cancel (true) , вызываемый поток правильно уведомляется о прерывании НО, если основной поток немедленно ожидает его завершение с использованием Future.get () , вызываемый поток имеет вид kill при вызове любого метода блокировки .

Следующий фрагмент кода JUnit 5 иллюстрирует проблему. Мы можем легко воспроизвести его, если основной поток не спит между вызовами 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();
    }

1 Ответ

0 голосов
/ 22 января 2019

Когда вы наберете get() для отмененного Future, вы получите CancellationException, следовательно, не будете ждать, пока код Callable выполнит его очистку. Затем вы только что вернулись, и наблюдаемое поведение уничтожаемых потоков, по-видимому, является частью очистки JUnit, когда он определил, что тест завершен.

Чтобы дождаться полной очистки, измените последнюю строку с

executorService.shutdown();

до

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

Обратите внимание, что в объявлении throws метода проще объявлять неожиданные исключения, чем загромождать ваш тестовый код предложениями catch, вызывающими Assertions.fail. В любом случае JUnit будет сообщать о таких исключениях как сбой.

Затем вы можете удалить весь код sleep.

Возможно, стоит включить управление ExecutorService в методы @Before / @After или даже @BeforeClass / @AfterClass, чтобы методы тестирования были свободны от этого, чтобы сосредоточиться на реальных тестах .¹


¹ Это были имена JUnit 4. IIRC, имена JUnit 5 похожи на @BeforeEach / @AfterEach соотв. @BeforeAll / @AfterAll

...