1)delay(ms):可取消的“睡眠”,不阻塞线程
-
作用:让当前协程挂起 ms 毫秒,不占用底层线程(调度器会在到时后恢复协程)。
-
可取消:期间如协程被取消,delay 会立刻结束并抛 CancellationException。
-
时间基准:使用单调时钟(不受系统时钟跳变影响)。
-
用法要点
- 仅在协程内可用;别在 flow {} 里用 withContext 切线程来“睡”,那会破坏 Flow 不变式。
- 在重试/退避、心跳、速率限制里最常见。
suspend fun fetchWithRetry(max: Int = 5) {
var backoff = 200L
repeat(max) { attempt ->
runCatching { fetch() }
.onSuccess { return }
delay(backoff) // 可取消的退避
backoff = (backoff * 2).coerceAtMost(10_000L)
}
error("still failing")
}
2)withTimeout/withTimeoutOrNull:给一段挂起代码加“总时限”
- withTimeout(ms) :超时会取消当前协程的子层级并抛 TimeoutCancellationException。
- withTimeoutOrNull(ms) :超时返回 null,避免抛异常(但内部仍被取消)。
- 适用:给 receive()、send()、网络/IO、select {} 整段加护栏。
- 注意:异常是取消异常;如果你 catch (e: Exception) 把它吞掉了,可能掩盖上层取消。要么不要捕获,要么只捕获业务异常,或检查 e is CancellationException 直接 throw e 继续向上。
// 等待通道数据最多 500ms
val item = withTimeoutOrNull(500) { ch.receive() }
if (item == null) showStale() else render(item)
// 给“发消息”加超时
withTimeout(300) { ch.send(cmd) } // 满缓冲+SUSPEND 时,send 会挂到超时
3)ticker(...):节拍器通道(ReceiveChannel)
让你以固定节奏收到 Unit“滴答”。非常适合心跳、速率限制、对齐帧率、周期采样。
fun ticker(
delayMillis: Long, // 相邻 tick 的间隔
initialDelayMillis: Long = delayMillis,
mode: TickerMode = FIXED_DELAY // FIXED_DELAY / FIXED_PERIOD
): ReceiveChannel<Unit>
-
模式
- FIXED_DELAY:每次消费者“取出”后再等 delay 发下一次(不会追赶)。
- FIXED_PERIOD:尽量对齐绝对时钟周期(追赶节拍;消费慢时会尽快补齐到期 tick)。
-
背压: “合流/覆盖”语义(类似 CONFLATED) ——不会无限排队;慢消费不会造成大量堆积。
-
停止:不再需要时 cancel() 之,或让外层协程结束。
val ticks = ticker(delayMillis = 200, initialDelayMillis = 0, mode = TickerMode.FIXED_PERIOD)
launch {
for (unit in ticks) pullAndRenderLatest() // 200ms 刷一次
}
// ...
ticks.cancel()
Flow 也有 sample(Period) 可替代“按周期取最新”;但 ticker 适合通道/选择器场景(见下)。
4)select {}:把“时间”与“通道事件”放到一个竞态里
select 是 “谁先可用选谁” 的原语。可监听:
-
ch.onReceive { ... } / onReceiveCatching { ... }
-
ch.onSend(value) { ... }
-
deferred.onAwait { ... }
-
onTimeout(ms) { ... } ← 时间分支
Kotlin 的 select 是“有偏好的” :从上到下注册分支,先匹配到先选。如果你需要“近似公平”,要轮换分支顺序或自己做均衡。
4.1 接收 or 超时,谁先到选谁
import kotlinx.coroutines.selects.select
suspend fun <T> recvOrTimeout(ch: ReceiveChannel<T>, timeoutMs: Long): T? =
select {
ch.onReceiveCatching { r ->
if (r.isClosed) null else r.getOrNull()
}
onTimeout(timeoutMs) { null } // 超时分支
}
4.2 两个源 + 超时:抢占先到的
val value = select<String?> {
chA.onReceive { "A:$it" }
chB.onReceive { "B:$it" }
onTimeout(1000) { null }
}
4.3 选择“能立刻发出去”的那条通道(发送竞争)
suspend fun sendAny(cmd: Cmd, a: SendChannel<Cmd>, b: SendChannel<Cmd>) =
select {
a.onSend(cmd) { "A" } // 哪个有空位选哪个
b.onSend(cmd) { "B" }
onTimeout(50) { error("all busy") } // 全忙则报错/重试
}
4.4 ticker + select:速率控制 + 兜底
val ticks = ticker(100, 0, TickerMode.FIXED_DELAY)
while (isActive) {
select {
ch.onReceive { process(it) } // 有消息就处理
ticks.onReceive { flushBatch() } // 没消息也每 100ms 刷一次
onTimeout(3_000) { markStale() } // 长时间没消息 → 标记超时
}
}
5) 典型时间×通道模式
5.1 心跳与看门狗(watchdog)
- 目标:周期发送 PING,一定时间没收到 PONG → 视为断线。
val pings = ticker(5_000)
while (isActive) {
select {
pings.onReceive { socket.send(PING) }
socket.onReceiveCatching { r ->
if (r.isClosed) closeConn() else if (r.getOrNull() == PONG) ack()
}
onTimeout(15_000) { reconnect() } // 15s 没任何消息
}
}
5.2 限速消费者(固定吞吐)
val pace = ticker(50) // 20条/秒
for (item in inbox) {
pace.receive() // 按节拍取许可
handle(item)
}
5.3 批处理(有数据就吃,空闲也按时刷)
val tick = ticker(200)
val buf = mutableListOf<Event>()
while (isActive) {
select {
inbox.onReceive { e ->
buf += e
if (buf.size >= 100) { flush(buf); buf.clear() }
}
tick.onReceive {
if (buf.isNotEmpty()) { flush(buf); buf.clear() }
}
}
}
5.4 “超时回退到旧数据”
val fresh = withTimeoutOrNull(150) { remoteCh.receive() }
val data = fresh ?: cache.loadLastKnownGood()
render(data)
6) 容量/背压与时间原语的配合
- withTimeout(send) + SUSPEND:当队列满,超过时限就放弃这条(或记录日志)。
- ticker + DROP_OLDEST:高频输入时滑窗 + 固定刷新节拍,保障 UI 不被顶爆。
- select { onTimeout } 比整段 withTimeout 更灵活:你可以不取消外层,只是走“超时分支”。
7) 易坑与建议
- 吞掉取消异常:withTimeout 抛的是 TimeoutCancellationException(CancellationException 子类)。别用 catch (Exception) 一股脑吞掉;若要处理,检测后 throw 回去。
- 用系统时间做节拍:请使用 delay/ticker(单调时钟),不要用 Thread.sleep/System.currentTimeMillis 做判断。
- ticker 不会无限积压:它是“覆盖式”的;如果你需要每个 tick 必达,请用 Channel(N,SUSPEND) + 生产者 send。
- select 的“偏好” :分支顺序会影响选择结果;多源竞争时可轮转顺序或人为均衡。
- 超时单位:所有超时/延时的单位统一用 Long 毫秒 或 Duration,避免单位误判。
- 优雅收尾:ticker.cancel();对自建 Channel 用 close() 让消费者把队列读干净。
8) 小抄(如何选)
| 目标 | 推荐 |
|---|---|
| 给某个操作加“总时限” | withTimeout / withTimeoutOrNull |
| 周期动作/帧率对齐 | ticker(period, initialDelay, mode) 或 Flow 的 sample(period) |
| 多事件竞态 + 超时兜底 | select { ch.onReceive ... ; onTimeout(t) { ... } } |
| 退避重试 | delay(backoff)(可取消) |
| 队列满时别一直挂起 | withTimeout { send(...) } 或 trySend + 丢弃策略 |