Kotlin-协程范围,为什么我的异步程序不被执行?

时间:2018-11-01 20:41:39

标签: asynchronous kotlin actor channel kotlinx.coroutines

CoroutineScopes如何工作?

假设我有一个

enum class ConceptualPosition{
    INVALID,
    A,B
}

假设我有一个用户可以从其中单击AB的UI。

我现在想要一个Actor接收用户输入,但是忽略它,直到实际请求输入为止。为了简单起见,我们假设只有一种方法请求职位。

sealed class PositionRequest{
    /**report the next position offered*/
    object ForwardNext:PositionRequest()
}

所以我们可以构造如下:

fun CoroutineScope.positionActor(
        offeredPosition:ReceiveChannel<ConceptualPosition>,
        requests:ReceiveChannel<PositionRequest>,
        output:SendChannel<ConceptualPosition>
) = launch{
    var lastReceivedPosition = INVALID
    var forwardNextReceived = 0

    println("ACTOR: entering while loop")
    while(true) {
        select<Unit> {
            requests.onReceive {
                println("ACTOR: requests.onReceive($it)")
                when (it) {
                    is PositionRequest.ForwardNext -> ++forwardNextReceived
                }
            }

            offeredPosition.onReceive {
                println("ACTOR: offeredPosition.onReceive($it)")
                lastReceivedPosition = it
                if (forwardNextReceived > 0) {
                    --forwardNextReceived
                    output.send(it)
                }
            }
        }
    }
}

然后构建一个外观与之交互:

class BasicUI{
    private val dispatcher = Dispatchers.IO

    /*start a Position Actor that receives input from the UI and forwards them on demand*/
    private val requests = Channel<PositionRequest>()
    private val offeredPositions = Channel<ConceptualPosition>()
    private val nextPosition = Channel<ConceptualPosition>()
    init {
        runBlocking(dispatcher){
            positionActor(offeredPositions,requests,nextPosition)
        }
    }

    /** Receives a [ConceptualPosition] that may or may not get accepted and acted upon.*/
    fun offerPosition(conceptualPosition: ConceptualPosition) = runBlocking(dispatcher) {
        offeredPositions.send(conceptualPosition)
    }

    /** waits for a [ConceptualPosition] to be offered via [offerPosition], then accepts it*/
    fun getPosition(): ConceptualPosition = runBlocking(dispatcher){
        requests.send(PositionRequest.ForwardNext)
        nextPosition.receive()
    }
}

这当然不起作用,因为runBlockingCoroutineScope,所以init不会返回,直到positionActor(offeredPositions,requests,nextPosition)发起的协程结束...永远不会因为里面有while(true)

那么,如果让BasicUI实现CoroutineScope怎么办?毕竟, what Roman Elizarov said we should do at the KotlinConf,如果我正确理解他的话,应该将positionActor(...)创建的协程绑定到BasicUI实例,而不是{{ 1}}块。

让我们看看...

runBlocking

让我们建立一个小的测试用例:我给演员提供一些import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlin.coroutines.CoroutineContext class BasicUI:CoroutineScope{ private val dispatcher = Dispatchers.IO private val job = Job() override val coroutineContext: CoroutineContext get() = job /*start a Position Actor that receives input from the UI and forwards them on demand*/ private val requests = Channel<PositionRequest>() private val offeredPositions = Channel<ConceptualPosition>() private val nextPosition = Channel<ConceptualPosition>() init { positionActor(offeredPositions,requests,nextPosition) } /** Receives a [ConceptualPosition] that may or may not get accepted and acted upon.*/ fun offerPosition(conceptualPosition: ConceptualPosition) = runBlocking(dispatcher) { offeredPositions.send(conceptualPosition) } /** waits for a [ConceptualPosition] to be offered via [offerPosition], then accepts it*/ fun getPosition(): ConceptualPosition = runBlocking(dispatcher){ requests.send(PositionRequest.ForwardNext) nextPosition.receive() } } ,他应该忽略它们,然后启动一个协程,持续提供A,其中一个将返回给我。当我要求演员担任职位时。

B

这将导致

import ConceptualPosition.*
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking

fun main(args: Array<String>) = runBlocking{
    val ui = BasicUI()
    println("actor engaged")

    //these should all be ignored
    repeat(5){ui.offerPosition(A)}
    println("offered some 'A's")

    //keep offering 'B' so that eventually, one will be offered after we request a position
    async { while(true){ui.offerPosition(B)} }

    //now get a 'B'
    println("requesting a position")
    val pos = ui.getPosition()
    println("received '$pos'")
}

...什么也没有。

显然,actor engaged ACTOR: entering while loop ACTOR: offeredPosition.onReceive(A) ACTOR: offeredPosition.onReceive(A) ACTOR: offeredPosition.onReceive(A) ACTOR: offeredPosition.onReceive(A) offered some 'A's ACTOR: offeredPosition.onReceive(A) requesting a position ACTOR: requests.onReceive(PositionRequest$ForwardNext@558da0e9) 从未提供过-因此也从未转发过-这导致主线程被阻塞(在这种情况下应该如此)。

我扔了一个

B

进入if(conceptualPosition == ConceptualPosition.B) throw RuntimeException("B offered?!") ,也没有例外,所以...

在这一点上,我可能不得不承认我还不了解Kotlin BasicUI.offerPosition

为什么这个例子不起作用?

1 个答案:

答案 0 :(得分:2)

这里似乎有两个问题:

  1. offerPosition / getPosition不是挂起函数。在大多数情况下,使用runBlocking是错误的解决方案,在必须与同步代码或主要功能进行接口连接时应使用该方法。
  2. 没有任何参数的
  3. async在当前的CoroutineScope中执行。对于您的主要功能,这是runBlocking。该文档实际上描述了行为:
  

事件循环的内部实现中此构建器的默认CoroutineDispatcher,该事件循环处理此阻塞线程中的继续操作,直到完成此协程。有关kotlinx.coroutines提供的其他实现,请参见CoroutineDispatcher。

简单来说,async块将不会轮到在事件循环中执行,而其他延续正在使用它。由于getPosition正在阻止,因此您会阻止事件循环。

将阻塞函数替换为暂停函数,并用withContext(dispatcher)来分派到其他执行程序上,将允许异步函数运行并最终解决状态。