Kotlin 让 Channel 的 send 操作不挂起

188 阅读2分钟

要让 Kotlin Channelsend 操作不挂起,核心思路是‌避免缓冲区满或未及时消费导致的挂起条件‌。以下是具体实现方法及策略:


一、配置缓冲区容量

  1. 设置 capacity 参数

    • 默认无缓冲‌(RENDEZVOUS)要求生产与消费协程同步,容易触发挂起16。
    • 增大缓冲区容量‌:通过 Channel(capacity) 指定足够大的缓冲区,例如 Channel<Int>(100),允许短时间内积累未消费数据67。
    • 无界缓冲区‌:使用 Channel.UNLIMITED,此时 send 永不挂起(需注意内存溢出风险)47。
  2. 示例代码

    val channel = Channel<Int>(capacity = Channel.UNLIMITED) // 无界缓冲区
    launch {
        repeat(1000) {
            channel.send(it) // 不会挂起
        }
    }
    
fun main():Unit = runBlocking {

    val channel = Channel<Int>(Channel.UNLIMITED)

    launch {
        (1..1000) .forEach{
            delay(2000)
            channel.send(it)
            println("生产一个:$it")
        }
    }

    //第一种消费方式
    launch {
//        (1..1000) .forEach{
//            delay(2000)
//            var it = channel.receive()
//            println("消费一个:$it")
//
//    }

    //第二种消费方式
    val it = channel.iterator()
        while (it.hasNext()){
            delay(2000)
            var it = channel.receive()
            println("消费一个:$it")
        }

    }


    //channel 是一个队列,队列是有缓冲区的,设置为 2147483647
    //缓冲区不可能满,receive 并没有消费掉,此时send 也不会挂起,因为会存入缓冲区

二、调整溢出策略

通过 onBufferOverflow 参数指定缓冲区满时的行为(需配合 capacity 使用)4:

  1. ‌**DROP_OLDEST**‌:丢弃最旧元素,腾出空间接收新元素。
  2. ‌**DROP_LATEST**‌:丢弃当前发送的新元素,保留已有数据。
  3. ‌**CONFLATED**‌(特殊类型):仅保留最新元素,自动覆盖旧值4。
val channel = Channel<Int>(
    capacity = 10,
    onBufferOverflow = BufferOverflow.DROP_OLDEST // 缓冲区满时丢弃旧元素
)

三、使用非阻塞替代方法

  1. ‌**trySend 方法**‌
    非挂起函数 trySend 立即返回 ChannelResult,可判断是否成功5:

    val result = channel.trySend(element)
    if (result.isSuccess) {
        // 发送成功
    } else {
        // 处理失败逻辑(如丢弃或重试)
    }
    
  2. 结合 offer 方法(已弃用,推荐 trySend
    旧版本中可用 offer,但新版本建议迁移至 trySend5。


四、选择适用场景的策略

策略适用场景注意事项
设置大容量/无界缓冲区高吞吐场景,允许延迟消费警惕内存溢出风险47
DROP_OLDESTCONFLATED实时数据处理,可容忍丢失部分历史数据需评估数据完整性需求4
trySend 非阻塞发送要求绝对非阻塞的异步场景需处理发送失败逻辑5

五、典型错误规避

  • 无限循环未关闭‌:即使使用无界通道,生产者完成后需调用 close(),否则消费者可能永久挂起38。
  • 数据丢失风险‌:DROP_* 策略会主动丢弃数据,需确保业务逻辑允许