【发布时间】:2019-11-13 08:05:26
【问题描述】:
按照约定 (1),Kotlin suspend 函数应该是非阻塞的。通常我们有旧的 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 的相同线程线程池)。但是协程作业取消不会中断performBlockingCode(),它依赖于线程中断。
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) }
}
如下所述,上面的代码将无法正常工作,因为没有调用 continuation.resumeWith()
版本 D:使用 CompletableFuture:它提供了一种在可完成完成时注册回调的方法: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()
}
}
你知道有什么更好的方法吗?
【问题讨论】:
-
你的第三种方法被打破了,它永远不会自行完成。你会发现完成是一项挑战,因为你从
Future得到的只是阻塞get()或join()。你需要一个CompletableFuture:supplyAsync(fn, executor)。 -
Kotlin 库支持这一点的方式是提供
CompletableFuture.await()。唉,调用future.cancel(false)是硬编码的。但我认为你可以简单地以不同的方式编写自己的扩展。在CompletableFuture上编写扩展更容易与其他代码组合,可以让您不必每次都编写可挂起的包装器。请记住,中断带有协程的线程通常是一种危险的做法,可能会导致意外接收者接收到中断信号。 -
我已经编辑了描述,添加了“版本 D”。这就是你实现它的方式吗? (我没有测试代码是否运行)。我也找到了这个答案:stackoverflow.com/a/58402108/2075875 这也应该是 Futures 的有效解决方案。 (我们针对的Android没有CompletableFuture)
-
是的,这看起来是个不错的解决方案。在没有
CompletableFuture的情况下,我会同意。
标签: kotlin coroutine kotlin-coroutines