协程系列(七)Channel

1,753 阅读2分钟

本文基于协程官方文档讲解,具体可查看here

channel主要用来协程间通信。

一、挂起函数send和receive

send和receive不匹配的情形:👇

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) channel.send(x * x)
    }
    repeat(4) {   //只接收4次
        if (!channel.isClosedForReceive) {
            printMsg("${channel.receive()}")
        }
    }
    printMsg("Done")
}

image.png 这里发送了5次,接收只有4次,导致send挂起。程序不能正常结束,很危险。

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) channel.send(x * x)
    }
    repeat(6) {  //多接收一次
        if (!channel.isClosedForReceive) {
            printMsg("${channel.receive()}")
        }
    }
    printMsg("Done")
}

image.png 这里发送了5次,接收6次,导致receive挂起。程序不能正常结束,很危险。

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) channel.send(x * x)
    }
    repeat(5) {   
        if (!channel.isClosedForReceive) {
            printMsg("${channel.receive()}")
        }
    }
    printMsg("Done")
}

image.png 发送和接收是匹配的,程序正常结束。

Q:我们一般用完channel,就让其关闭的,但是接收和发送不匹配时,会一直挂起,程序没法结束,咋办?
可以在合适的时机主动close。

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) {
            if (!channel.isClosedForSend) {
                channel.send(x * x)
            }
            if (x > 3) {
                channel.close() 
                break
            }
        }
    }
    repeat(7) {
        printMsg("${channel.receiveCatching().getOrNull()}")
    }
    printMsg("Done")
}

image.png 多出了两次receive,我们可以添加判断解决channel.isClosedForReceive。

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) {
            if (!channel.isClosedForSend) {
                channel.send(x * x)
            }
            if (x > 3) {
                channel.close()
                break
            }
        }
    }
    repeat(7) {
        if(!channel.isClosedForReceive){  //加多一个判断
            printMsg("${channel.receiveCatching().getOrNull()}")
        }
    }
    printMsg("Done")
}

image.png 当遇到channel关闭时,使用receive接收数据尤其小心,可能会导致奔溃,建议使用channel.receiveCatching().getOrNull()

二、channel支持迭代接收

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) {
            channel.send(x * x)
            if (x > 3) {
                channel.close()
                break
            }
        }
    }
    for (y in channel) {   //支持迭代遍历接收
        printMsg("${y}")
    }
    printMsg("Done")
}

image.png 当然这里的迭代也可以用consumeEach

//接收
channel.consumeEach {
    printMsg("${it}")
}

三、使用produce构建channel

fun CoroutineScope.produceSquares(): ReceiveChannel<Int> = produce {
    for (x in 1..5) send(x * x)
}
fun main() = runBlocking {
    val squares = produceSquares()
    squares.consumeEach { printMsg("${it}") }
    printMsg("Done")
}

image.png

四、ReceiveChannel再次处理

一个协程产生多个流式值,另外一个协程可以对其进行处理,产生新值,最后可以消费。

//一个协程产生很多流式的值
fun CoroutineScope.produceNumbers(): ReceiveChannel<Int> = produce {
    var x = 1
    while (true) send(x++)
}

//另外一个协程对其进行处理,产生新的值
fun CoroutineScope.square(nums: ReceiveChannel<Int>) = produce {
    for (x in nums) send(x * x)
}

fun main() = runBlocking {
    val job = launch {
        val nums = produceNumbers()
        val squares = square(nums)
        repeat(5) {
            printMsg("${squares.receive()}")
        }
    }
    job.join()
    printMsg("Done")
}

image.png 这个channel程序又没能执行完,很危险的,实际开发过程中,要避免。怎么避免,如果channel不好关闭,就cancel当前channel所在的协程(coroutineContext[Job]?.cancel())。

fun main() = runBlocking {
    val job = launch {
        val nums = produceNumbers()
        val squares = square(nums)
        repeat(5) {
            printMsg("${squares.receiveCatching().getOrNull()}")
        }
        coroutineContext[Job]?.cancel()  //取消当前所在的协程,可以终止程序!!!!!!
    }

    job.join()
    printMsg("Done")
}

image.png

五、一个协程发射,多个协程接收,是线程安全的。

//不断的发射数据
fun CoroutineScope.produceNumbers(): ReceiveChannel<Int> = produce {
    var x = 1
    while (true) {
        send(x++)
        delay(100)
    }
}
fun CoroutineScope.launchProcessor(id: Int, nums: ReceiveChannel<Int>) = launch {
    for (msg in nums) {
        printMsg("processorId $id    ${msg}")      
    }
}
fun main() = runBlocking {
    val nums = produceNumbers()     //当前协程
    repeat(5) {    //开启5个子协程消费
        launchProcessor(it, nums)
    }
    delay(950)
    nums.cancel()
}

image.png

六、多个协程发射,一个协程接收。

suspend fun sendString(channel: SendChannel<String>, s: String, time: Long) {
    while (true) {
        delay(time)
        channel.send(s)
    }
}
fun main() = runBlocking {
    launch {
        val channel = Channel<String>()
        launch { sendString(channel, "foo", 200L) }
        launch { sendString(channel, "Bar", 500L) }
        repeat(6) {
            printMsg("${channel.receive()}")
        }
        coroutineContext[Job]?.cancel()  //及时终止channel,通过取消当前协程
    }.join()
}

image.png