Flow 與執行緒切換#
Flow 屬於 coroutine 範疇,自然也希望能享受 coroutine 的一個重要特性:輕鬆切換執行緒。在 coroutine 中我們會用 Dispatchers 搭配 coroutine builder(launch、async),或用 withContext 來切換到合適的執行緒/執行緒池。
那 Flow 是否也能在建立時直接帶入 Dispatcher 呢?看一下 flow 的簽名:
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T>很可惜,flow { ... } 並沒有 CoroutineContext 參數,所以沒辦法在建立時帶入 Dispatcher。
為什麼不能直接用 withContext#
直覺上會想:那我直接在 flow 內部用 withContext 包住 emit,是否可行?
class Day20 {
fun flow(): Flow<Int> = flow {
withContext(Dispatchers.Default) {
repeat(10) {
emit(it)
}
}
}
}fun main() = runBlocking {
val day20 = Day20()
day20.flow().collect { println(it) }
println("done")
}執行會直接拋出錯誤:Flow invariant is violated。錯誤訊息也會建議改用 flowOn。
Flow 對
emit的執行緒有嚴格規則:上游 emit 端與下游 collect 端的 context 必須一致,不能在 flow 內部用withContext任意切換執行緒。
改用 flowOn#
把 withContext 改成 flowOn:
class Day20 {
fun flow(): Flow<Int> = flow {
repeat(10) {
emit(it)
}
}.flowOn(Dispatchers.Default)
}執行結果:
0
1
2
3
4
5
6
7
8
9
doneflowOn 的簽名:
fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T>它接受 CoroutineContext,所以可以丟入 Dispatchers,讓 Flow 切換到對應的執行緒/執行緒池。
flowOn 只影響上游#
flowOn 的另一個重要特性:它只會影響「呼叫它之前」的所有操作,後面的部分不受影響。所以可以連續使用多個 flowOn 來分段切換執行緒:
class Day20 {
fun flow(): Flow<Int> = flow {
repeat(10) {
emit(it)
}
}
}fun main() = runBlocking {
val day20 = Day20()
val flow = day20.flow()
flow.map {
println("map: ${Thread.currentThread().name}")
it * 3
}
.flowOn(Dispatchers.Default)
.filter {
println("filter: ${Thread.currentThread().name}")
it % 2 == 0
}
.flowOn(Dispatchers.IO)
.collect {
println("collect: ${Thread.currentThread().name}")
println(it)
}
println("done")
}輸出(節錄):
map: DefaultDispatcher-worker-2
...
filter: DefaultDispatcher-worker-1
...
collect: main
0
collect: main
6
...
done從輸出可以看出:
map跑在DefaultDispatcher(受第一個flowOn(Dispatchers.Default)影響)。filter跑在另一個DefaultDispatcher(這裡實際上是 IO dispatcher 共用的 worker,仍與下游不同)。collect回到 main,這是runBlocking外層的 context。
預設使用外層的 Dispatcher#
如果沒有呼叫 flowOn,Flow 預設會使用「呼叫 collect 的那個 coroutine」的 Dispatcher:
class Day20 {
val scope = CoroutineScope(Job() + Dispatchers.Default)
fun flow2() = scope.launch {
flow {
println("emit: ${Thread.currentThread().name}")
emit(10)
}.collect {
println("collect: ${Thread.currentThread().name} $it")
}
}
}fun main() = runBlocking {
val day20 = Day20()
day20.flow2()
delay(100)
println("done")
}emit: DefaultDispatcher-worker-1
collect: DefaultDispatcher-worker-1 10
done這裡的 scope 帶的 Dispatchers.Default,emit 與 collect 都跑在同一條 worker 上。
launchIn 與 flowOn#
當搭配 launchIn 使用時,Flow 會被啟動在傳入的 CoroutineScope 中,可以再用 flowOn 進一步控制上游的執行緒。
小結#
- 不能在
flow { ... }中用withContext切換執行緒,會拋出「Flow invariant is violated」。 - 想切換 Flow 的執行緒就用
flowOn,它接受CoroutineContext。 flowOn只影響它「之前」的中間運算子;可以連續多次使用,分段控制不同階段的執行緒。- 如果沒有
flowOn,Flow 預設會跟著外層 coroutine 的 Dispatcher 跑。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10272159