Функции Kotlin suspend должны быть неблокирующими по соглашению (1). Часто у нас есть старый код Java, который полагается на механизм прерывания потока java, который мы не можем (не хотим) модифицировать (2):

public void doSomething(String arg) {
    for (int i = 0; i < 100_000; i++) {
        heavyCrunch(arg, i);
        if (Thread.interrupted()) {
            // We've been interrupted: no more crunching.
            return;
        }
    }
}

Как лучше всего адаптировать этот код для использования в сопрограммах?

Версия A: неприемлема, так как код будет выполняться в вызывающем потоке. Таким образом, это нарушит соглашение "приостановка функций не блокирует поток вызывающей стороны":

suspend fun doSomething(param: String) = delegate.performBlockingCode(param)

Версия B: лучше, потому что она будет запускать функцию блокировки в фоновом потоке, поэтому она не будет блокировать поток вызывающей стороны (за исключением случаев, когда вызывающая сторона случайно использует тот же поток из пула потоков Dispatchers.Default) . Но отмена задания сопрограммы не прерывает функцию executeBlockingCode(), которая зависит от прерывания потока.

suspend fun doSomething(param: String) = withContext(Dispatchers.Default) {
    delegate.performBlockingCode(param)
}

Версия C: в настоящее время это единственный способ заставить его работать. Идея состоит в том, чтобы преобразовать блокирующую функцию в неблокирующую с помощью механизмов Java, а затем использовать suspendCancellableCoroutine (3) для преобразования асинхронного метода в функцию приостановки:

private ExecutorService executor = Executors.newSingleThreadExecutor();

public Future doSomethingAsync(String arg) {
    return executor.submit(() -> {
        doSomething(arg);
    });
}

suspend fun doSomething(param: String) = suspendCancellableCoroutine<Any> { cont ->
    try {
        val future = delegate.doSomethingAsync(param)
    } catch (e: InterruptedException) {
        throw CancellationException()
    }
    cont.invokeOnCancellation { future.cancel(true) }
}

Как указано ниже, приведенный выше код не будет работать должным образом, поскольку не вызывается continue.resumeWith()

Версия D: использует CompletableFuture:, который предоставляет способ регистрации обратного вызова при завершении completable: thenAccept

private ExecutorService executor = Executors.newSingleThreadExecutor();

public CompletableFuture doSomethingAsync(String arg) {
    return CompletableFuture.runAsync(() -> doSomething(arg), executor);
}

suspend fun doSomething(param: String) = suspendCancellableCoroutine<Any> { cont ->
    try {
        val completableFuture = delegate.doSomethingAsync(param)
        completableFuture.thenAccept { cont.resumeWith(Result.success(it)) }
        cont.invokeOnCancellation { completableFuture.cancel(true) }
    } catch (e: InterruptedException) {
        throw CancellationException()
    }
}

Знаете ли вы лучший способ для этого?

  1. https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html
  2. https://medium.com/@elizarov/blocking-threads-suspending-coroutines-d33e11bf4761
  3. https://medium.com/@elizarov/callbacks-and-kotlin-flows-2b53aa2525cf
3
Malachiasz 13 Ноя 2019 в 11:05
Ваш третий подход сломан, он никогда не завершается сам по себе. Вы столкнетесь с проблемой завершения, потому что все, что вы получаете от Future, — это блокировка get() или join(). Вам нужен CompletableFuture: supplyAsync(fn, executor).
 – 
Marko Topolnik
13 Ноя 2019 в 12:31
Библиотека Kotlin поддерживает это, предоставляя CompletableFuture.await(). Увы, вызов future.cancel(false) жестко запрограммирован. Но я думаю, что вы могли бы просто написать собственное расширение по-другому. Написание расширения на CompletableFuture более совместимо с другим кодом, что может избавить вас от необходимости каждый раз писать приостанавливаемую оболочку. Имейте в виду, что прерывание потоков, несущих сопрограмму, в целом является опасной практикой и может привести к тому, что сигнал прерывания будет получен непреднамеренным получателем.
 – 
Marko Topolnik
13 Ноя 2019 в 12:52
Я отредактировал описание, добавил "Версия D". Вы так это реализуете? (Я не проверял, работает ли код). Также я нашел этот ответ: stackoverflow.com/a/58402108/2075875 Это также должно быть рабочим решением с фьючерсами. . (Android, на который мы нацелены, не имеет CompletableFuture)
 – 
Malachiasz
13 Ноя 2019 в 13:25
Да, это похоже на хорошее решение. В отсутствие CompletableFuture я бы выбрал это.
 – 
Marko Topolnik
13 Ноя 2019 в 14:18

1 ответ

Вы можете обернуть блокирующий код через suspend fun kotlinx.coroutines.runInterruptible

Он подавлял предупреждение о компиляции, а код блокировки выдавал InterruptedException при отмене.

val job = launch {
  runInterruptible {
    Thread.sleep(500)
  }
}

job.cancelAndJoin() // Cause will be 'java.lang.InterruptedException'

Протестировано на org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.2

1
Boken 28 Ноя 2020 в 03:14