接《Android開發者快速上手Kotlin(七) 之 協程官方框架初步》文章繼續。
14 協程官方框架Channel、Select和Flow
14.1 Channel
Channel我們一般翻譯成叫通道,用於多個協程之間進行數據相互傳輸,多個協程允許發送和接收同一個Channel的數據。它類似於線程任務隊列BlockingQueue + 掛起函數的支持,因爲如果通道支持緩存的話,那麼它實質上就是一個隊列。我們發消息和接收消息都是掛起函數,掛起取決於Channel的狀態,如果Channel已經滿了,那麼Send時就會被掛起,如果Channel裏什麼都沒有的話,那麼Receive時也會被掛起。
14.1.1 Channle的分類
我們從Channel函數的源碼可見,它有5種分類,源碼如下:
public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
when (capacity) {
RENDEZVOUS -> RendezvousChannel()
UNLIMITED -> LinkedListChannel()
CONFLATED -> ConflatedChannel()
BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY)
else -> ArrayChannel(capacity)
}
RENDEZVOUS
表示約會形式的等待,send調用後就會一直掛起,直到receive到達。
UNLIMITED
表示執行緩存無限容量,send調用後就存放在channel裏直接返回,不管是否有receive。但是我們在使用時還是需要注意內存情況。
CONFLATED
表示保留最新,send調用後就存放在channel裏直接返回,但是channel裏只能存放最近一次send的值。
BUFFERED
表示執行緩存使用默認容量,默認是64。
FIXED
表示執行緩存使用固定容量,跟BUFFERED一樣,只是容量值是通過參數自己傳入。
示例
fun main() = runBlocking {
val channel = Channel<Int>(Channel.RENDEZVOUS)
val producer = GlobalScope.launch {
for (i in 0..3) {
println("【${Thread.currentThread().name}】準備發送 $i")
channel.send(i)
println("【${Thread.currentThread().name}】發送完畢 $i")
}
channel.close()
}
val consumer = GlobalScope.launch {
while (!channel.isClosedForReceive) { // 還可以繼續接收
println("【${Thread.currentThread().name}】準備接收")
val value = channel.receiveOrNull() // 跟receive的區別在於使用receive話channel如果被close會拋出異常
println("【${Thread.currentThread().name}】接收完畢 $value")
}
}
producer.join()
consumer.join()
}
運行結果
【DefaultDispatcher-worker-1】準備發送 0
【DefaultDispatcher-worker-2】準備接收
【DefaultDispatcher-worker-2】接收完畢 0
【DefaultDispatcher-worker-2】準備接收
【DefaultDispatcher-worker-1】發送完畢 0
【DefaultDispatcher-worker-1】準備發送 1
【DefaultDispatcher-worker-1】發送完畢 1
【DefaultDispatcher-worker-1】準備發送 2
【DefaultDispatcher-worker-2】接收完畢 1
【DefaultDispatcher-worker-2】準備接收
【DefaultDispatcher-worker-2】接收完畢 2
【DefaultDispatcher-worker-2】準備接收
【DefaultDispatcher-worker-1】發送完畢 2
【DefaultDispatcher-worker-1】準備發送 3
【DefaultDispatcher-worker-1】發送完畢 3
【DefaultDispatcher-worker-2】接收完畢 3
【DefaultDispatcher-worker-2】準備接收
【DefaultDispatcher-worker-1】接收完畢 null
解說
- 有些情況下打印順序不一致,那是因爲兩個協程是運行在兩個線程中,它們的執行順序取決於CPU對線程的調度,但是能保證的是,每次發送後,如果沒有接收定會被掛起,如果沒有發送,那麼接收地方一定會被掛起。
14.1.2 Channle的關閉
1. 調用Channel的close函數後,通道就會被關閉。
2. 通道關閉後,isClosedForSend函數就會返回true,此時如果繼續調用send函數就會拋出ClosedSendChannelException異常。
3. 通道關閉後,調用receive仍可接收緩存的數據,直到緩存數據消費完後,isClosedForReceive函數就會返回true,此時繼續調用receive函數就會拋出ClosedReceiveChannelExceptoin異常。
14.1.3 Channel的迭代
Channel類似於BlockingQueue ,所以它可以進行循環迭代也是情理之中, 它內部實現了ChannelIterator接口的掛起函數hasNext。
hasNext在有緩存的數據時會返回true;
hasNext在未關閉且緩存爲空時掛起;
hasNext在正常關閉且緩存爲空時返回false。
所以我們在上述示例中,將while進行註釋後換成foreach的方式進行也是可以達到一樣的效果的:
val consumer = GlobalScope.launch {
// while (!channel.isClosedForReceive) { // 還可以繼續接收
// println("【${Thread.currentThread().name}】準備接收")
// val value = channel.receiveOrNull() // 跟receive的區別在於使用receive話channel如果被close會拋出異常
// println("【${Thread.currentThread().name}】接收完畢 $value")
// }
for (i in channel) {
println("【${Thread.currentThread().name}】接收 $i")
}
}
14.1.4 Channel的協程Buidler(SendChannel / ReceiveChannel)
我們在上面示例中可見,通過一個生產者協程producer和一個消費者協程consumer進行了數據的send和receive,而在官方框架中也專門爲生產者協程和消費者協程提供了兩個函數來構建出協程,它們就是produce和actor。而且通過produce和actor函數啓動的協程結束後都會自動關閉對應的Channel。
produce:啓動一個生產者協程,返回ReceiveChannel。
actor:啓動一個消息者協程,返回SendChannel(注意,actor函數目前框架中是被標爲廢棄)。
示例1,ReceiveChannel
fun main() = runBlocking {
val receiveChannel = GlobalScope.produce(capacity = Channel.RENDEZVOUS) {
for (i in 0..3) {
println("【${Thread.currentThread().name}】準備發送 $i")
send(i) // 等價於channel.send(i)
println("【${Thread.currentThread().name}】發送完畢 $i")
}
}
val consumer = GlobalScope.launch {
for (i in receiveChannel) {
println("【${Thread.currentThread().name}】接收 $i")
}
}
consumer.join()
}
示例2,SendChannel
fun main() = runBlocking {
val sendChannel = GlobalScope.actor<Int>(capacity = Channel.RENDEZVOUS ) {
for (i in this) {
println("【${Thread.currentThread().name}】接收 $i")
}
}
val producer = GlobalScope.launch {
for (i in 0..3) {
println("【${Thread.currentThread().name}】準備發送 $i")
sendChannel.send(i)
println("【${Thread.currentThread().name}】發送完畢 $i")
}
}
producer.join()
}
運行結果
【DefaultDispatcher-worker-1】準備發送 0
【DefaultDispatcher-worker-1】發送完畢 0
【DefaultDispatcher-worker-2】接收 0
【DefaultDispatcher-worker-1】準備發送 1
【DefaultDispatcher-worker-1】發送完畢 1
【DefaultDispatcher-worker-1】準備發送 2
【DefaultDispatcher-worker-2】接收 1
【DefaultDispatcher-worker-2】接收 2
【DefaultDispatcher-worker-1】發送完畢 2
【DefaultDispatcher-worker-1】準備發送 3
【DefaultDispatcher-worker-1】發送完畢 3
【DefaultDispatcher-worker-2】接收 3
14.1.5 BroadcastChannel
前面介紹的Channel的所發送的數據只能被一個消費者消費,而如果需要一對多的話那就需要BroadcastChannel,它會像我們平時使用廣播一樣進行分發給所有訂閱者。另外需要注意的是,BroadcastChannel不支持RENDEZVOUS。
示例
fun main() = runBlocking {
val broadcastChannel = GlobalScope.broadcast {
for (i in 0..3) {
println("【${Thread.currentThread().name}】準備發送 $i")
send(i)
println("【${Thread.currentThread().name}】發送完畢 $i")
}
}
List(3){index ->
GlobalScope.launch{
for (i in broadcastChannel.openSubscription()) {
println("【${Thread.currentThread().name}】協程$index 接收 $i")
}
}
}.joinAll()
}
運行結果
【DefaultDispatcher-worker-2】準備發送 0
【DefaultDispatcher-worker-2】發送完畢 0
【DefaultDispatcher-worker-4】協程1 接收 0
【DefaultDispatcher-worker-2】準備發送 1
【DefaultDispatcher-worker-3】協程2 接收 0
【DefaultDispatcher-worker-2】發送完畢 1
【DefaultDispatcher-worker-1】協程0 接收 0
【DefaultDispatcher-worker-2】準備發送 2
【DefaultDispatcher-worker-3】協程2 接收 1
【DefaultDispatcher-worker-2】發送完畢 2
【DefaultDispatcher-worker-3】協程2 接收 2
【DefaultDispatcher-worker-1】協程0 接收 1
【DefaultDispatcher-worker-3】協程1 接收 1
【DefaultDispatcher-worker-2】準備發送 3
【DefaultDispatcher-worker-3】協程1 接收 2
【DefaultDispatcher-worker-1】協程0 接收 2
【DefaultDispatcher-worker-3】協程1 接收 3
【DefaultDispatcher-worker-1】協程0 接收 3
【DefaultDispatcher-worker-3】協程2 接收 3
【DefaultDispatcher-worker-2】發送完畢 3
14.2 Select
Select一般是IO多路複用的概念,而在協程的Select則是用於掛起函數的多路複用。通俗一點表達就是可以同時進行多個掛起函數的調用,但最後只選擇執行最快的掛起函數的返回結果。
示例1
fun main() = runBlocking {
val one = async(Dispatchers.Default) { doOne() }
val two = doTwo()
select<Unit> { // Unit表示 select 表達式不返回任何結果
one.onAwait { value ->
println("【${Thread.currentThread().name}】one -> $value")
}
two.onReceive { value ->
println("【${Thread.currentThread().name}】two -> $value")
}
}
}
suspend fun doOne(): Int {
delay(1000)
println("【${Thread.currentThread().name}】doOne 計算中")
return 1
}
fun doTwo() = GlobalScope.produce<Int> {
delay(500)
println("【${Thread.currentThread().name}】doTwo 計算中")
send(2)
}
運行結果1
【DefaultDispatcher-worker-2】doTwo 計算中
【main】two -> 2
【DefaultDispatcher-worker-2】doOne 計算中
示例2
fun main() = runBlocking {
val one = async(Dispatchers.Default) { doOne() }
val two = doTwo()
select<Unit> { // Unit表示 select 表達式不返回任何結果
one.onAwait { value ->
println("【${Thread.currentThread().name}】one -> $value")
}
two.onReceive { value ->
println("【${Thread.currentThread().name}】two -> $value")
}
}
}
suspend fun doOne(): Int {
delay(500)
println("【${Thread.currentThread().name}】doOne 計算中")
return 1
}
fun doTwo() = GlobalScope.produce<Int> {
delay(1000)
println("【${Thread.currentThread().name}】doTwo 計算中")
send(2)
}
運行結果2
【DefaultDispatcher-worker-1】doOne 計算中
【main】one -> 1
解說
- 示例1和示例2僅僅是兩個掛起函數delay時長的區別,在示例1中,doTwo比doOne函數快,所以打印出2,示例2中,它們的delay時長剛纔相反,所以打印出的值是1。
- 我們從兩個運行結果中還發現,運行結果1打印了3行,表示需然最終採納的結果是onOne的值,但是onTwo還是堅持執行完了。而運行結果2打印了2行,表示最終採納的結果是onTwo的值,但是onOne被中止了。
- 在調用doOne函數使用了async返回了一個Deferred,所以我們可以使用.await()對它進行結果的等待,而在select中變成相應的onAwait()。
- 在調用doTwo函數時,因爲它是一個Channle,所以在select中使用了onReceive對其進行結果接收。
14.3 Flow
我們在使用掛起函數處理異步操作時它只能返回單個結果,而Flow我們一般叫它異步流,它就可以在掛起函數處理異步計算時返回多個結果。它在使用上跟sequence(序列)非常像,sequence是協程語言級的API,sequence不能使用delay,它只會阻塞當前線程。如:
fun main() {
val foo = sequence { // 序列構建器
for (i in 1..3) {
yield(i) // 產生下一個值
Thread.sleep(100)
}
}
foo.forEach { value -> println(value) }
}
所以官方協程框架爲了解決像sequence使用場景中能使用delay不阻塞線程就出現了Flow。
14.3.1 Flow基本用法
示例
fun main() = runBlocking {
val foo = flow {
for (i in 1..3) {
println("【${Thread.currentThread().name}】flow $i")
emit(i)
delay(100)
}
}
foo.collect{ value -> println("【${Thread.currentThread().name}】collect $value") }
}
運行結果
【main】flow 1
【main】collect 1
【main】flow 2
【main】collect 2
【main】flow 3
【main】collect 3
解說
- 使用flow需要import kotlinx.coroutines.flow.*。
- 使用的collect函數觸發flow裏代碼的執行從而讀flow內發射回來的值。
14.3.2 Flow的創建
除了上面示例中使用flow可以用於創建 flow,還可從從集合或者從Channel中去創建Flow。
示例
fun main() = runBlocking {
listOf(1, 2, 3).asFlow()
.onEach {
delay(100)
}.collect {
println("通過 asFlow 創建的 Flow $it")
}
flowOf(1, 2, 3)
.onEach {
delay(100)
}
.collect {
println("通過 flowOf 創建的 Flow $it")
}
channelFlow {
for (i in 1..3) {
delay(100)
send(i)
}
}.collect {
println("通過 channelFlow 創建的 Flow $it")
}
}
運行結果
通過 asFlow 創建的 Flow 1
通過 asFlow 創建的 Flow 2
通過 asFlow 創建的 Flow 3
通過 flowOf 創建的 Flow 1
通過 flowOf 創建的 Flow 2
通過 flowOf 創建的 Flow 3
通過 channelFlow 創建的 Flow 1
通過 channelFlow 創建的 Flow 2
通過 channelFlow 創建的 Flow 3
解說
- 通過asFlow和flowOf對集合進行創建Flow。
- 通過channelFlow可以從Channel創建Flow。
14.3.3 Flow使用調度器切換線程
示例
fun main() = runBlocking {
val foo = flow {
for (i in 1..3) {
println("【${Thread.currentThread().name}】flow $i")
emit(i)
delay(100)
}
}
foo.flowOn(Dispatchers.IO).collect{ value -> println("【${Thread.currentThread().name}】collect $value") }
}
運行結果
【DefaultDispatcher-worker-1】flow 1
【main】collect 1
【DefaultDispatcher-worker-1】flow 2
【main】collect 2
【DefaultDispatcher-worker-1】flow 3
【main】collect 3
解說
- 通過flowOn函數,跟使用launch一樣傳入相應的調度器就可以進行線程的切換。
14.3.4 Flow的異常處理
示例
fun main() = runBlocking {
val foo = flow {
emit(1)
throw ArithmeticException("計算異常了")
emit(2)
}.catch { t:Throwable->
println("【${Thread.currentThread().name}】catch error: $t")
emit(-1)
}.onCompletion { t:Throwable?->
println("【${Thread.currentThread().name}】onCompletion: $t")
}
foo.collect{ value -> println("【${Thread.currentThread().name}】collect $value") }
}
運行結果
【main】collect 1
【main】catch error: java.lang.ArithmeticException: 計算異常了
【main】collect -1
【main】onCompletion: null
解說
- flow表達式後可以直接通過.catch進行異常的捕獲,但不包括取消異常,因爲取消操作屬於正常邏輯並不算真正意義上的異常。
- onCompletion類似於我們平時異常捕獲中的finally,它是一定會執行的,t是否爲null取決於是否有異常和是否前面catch是否有將異常捕獲。
- 如果我們在flow { ... } 構建器內部的 try/catch來捕獲異常也是可以的,但是我們不建議這樣做,因爲會違反異常透明性的,而且這樣做我們並不能在catch中繼續使用emit來發射值。
14.3.5 Flow的取消
Flow本身並沒有取消的API,因爲Flow的運行依賴於協程,Flow的取消取決於collect所在的協程的取消,collect作爲掛起函數可以響應所在協程的取消狀態。
示例
fun main() = runBlocking {
val foo = flow {
emit(1)
delay(1000)
emit(2)
}
withTimeoutOrNull(200) {
foo.collect{ value -> println("【${Thread.currentThread().name}】collect $value") }
}
println("【${Thread.currentThread().name}】Main函數結束")
}
運行結果
【main】collect 1
【main】Main函數結束
14.3.6 Flow元素併發問題
如果我們在創建一個Flow後想在裏面進行通過調度器切換線程是不允許的,因爲emit本身並不是線程安全的。如果你非要這樣做的話,可以選擇使用channelFlow來創建Flow,因爲Channel是一個併發安全的消息通道,send本身是線程安全的。
示例1
fun main() = runBlocking {
flow {
emit(1)
withContext(Dispatchers.IO) {
emit(2)
}
}.collect{ value -> println("【${Thread.currentThread().name}】collect $value") }
}
運行結果1
【main】collect 1
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [BlockingCoroutine{Active}@3129481d, BlockingEventLoop@3060e7dc],
but emission happened in [DispatchedCoroutine{Active}@f7f6781, LimitingDispatcher@65d440f2[dispatcher = DefaultDispatcher]].
Please refer to 'flow' documentation or use 'flowOn' instead
……
示例2
fun main() = runBlocking {
channelFlow {
send(1)
withContext(Dispatchers.IO) {
send(2)
}
}.collect{ value -> println("【${Thread.currentThread().name}】collect $value") }
}
運行結果2
【main】collect 1
【main】collect 2
14.3.7 Flow的緩衝
當發射太快而消費太慢的時候,由於消費的速度跟不上發射的速度,這時就會影響到後面結果的發射。
示例1
fun main() = runBlocking {
flow {
for (i in 1..3) {
delay(100)
println("【${Thread.currentThread().name}】flow $i")
emit(i)
}
}.collect { value ->
delay(1000)
println("【${Thread.currentThread().name}】collect $value")
}
}
輸入結果1
【main】flow 1
【main】collect 1
【main】flow 2
【main】collect 2
【main】flow 3
【main】collect 3
示例2
fun main() = runBlocking {
flow {
for (i in 1..3) {
delay(100)
println("【${Thread.currentThread().name}】flow $i")
emit(i)
}
}.buffer().collect { value ->
delay(1000)
println("【${Thread.currentThread().name}】collect $value")
}
}
輸入結果2
【main】flow 1
【main】flow 2
【main】flow 3
【main】collect 1
【main】collect 2
【main】collect 3
解說
1. 示例2中我們在流中加入了buffer後,當消費未完成時,先由buffer來緩衝發射項,這樣往後需要發射的結果就無需等待。
14.3.7 Flow的背壓問題
buffer僅能緩解發射太快而消費太慢的問題,但是它還是會存在buffer滿了的情況。這類背壓的問題還可以使用conflate或者collectLatest來進行解決。
示例1,conflate合併
fun main() = runBlocking {
flow {
for (i in 1..3) {
delay(100)
println("【${Thread.currentThread().name}】flow $i")
emit(i)
}
}.conflate().collect { value ->
delay(1000)
println("【${Thread.currentThread().name}】collect $value")
}
}
運行結果1
【main】flow 1
【main】flow 2
【main】flow 3
【main】collect 1
【main】collect 3
解說1
1. conflate的調用後會生成一個新的flow,當流操作結果或操作狀態更新時,可能沒有必要處理每個值,而是隻處理最新的那個,這時就可以使用conflate來跳過中間值,只保留最新值。
示例2, collectLatest處理最新值
fun main() = runBlocking {
flow {
for (i in 1..3) {
delay(100)
println("【${Thread.currentThread().name}】flow $i")
emit(i)
}
}.collectLatest { value ->
println("【${Thread.currentThread().name}】collecting $value")
delay(1000)
println("【${Thread.currentThread().name}】collected $value")
}
}
運行結果2
【main】flow 1
【main】collecting 1
【main】flow 2
【main】collecting 2
【main】flow 3
【main】collecting 3
【main】collected 3
解說2
- 使用conflate合併是加快處理速度的一種方式。它通過刪除發射值來實現。 另一種方式就是使用collectLatest取消緩慢的收集器,並在每次發射新值的時候重新啓動它。
14.3.8 Flow的組合
示例1, zip組合兩個流的值
fun main() = runBlocking {
val nums = (1..3).asFlow()
val strs = flowOf("one", "two", "three")
nums.zip(strs) { a, b ->
"$a -> $b"
}.collect { value ->
println("【${Thread.currentThread().name}】$value")
}
}
運行結果1
【main】1 -> one
【main】2 -> two
【main】3 -> three
解說1
- 對流使用zip可用於組合兩個流中的相關值。
示例2,combine結合計算
fun main() = runBlocking {
val nums = (1..3).asFlow()
.onEach {
delay(300)
println("【${Thread.currentThread().name}】nums: $it")
}
val strs = flowOf("one", "two", "three")
.onEach {
delay(400)
println("【${Thread.currentThread().name}】strs: $it")
}
nums.combine(strs) { a, b ->
"$a -> $b"
}.collect { value ->
println("【${Thread.currentThread().name}】$value")
}
}
輸入結果2
【main】nums: 1
【main】strs: one
【main】1 -> one
【main】nums: 2
【main】2 -> one
【main】strs: two
【main】2 -> two
【main】nums: 3
【main】3 -> two
【main】strs: three
【main】3 -> three
解說2
- 假如兩個流執行的時間並非一致,將zip換成combine後,每當流產生值的時候都需要重新計算。
15 總結
Kotlin裏關於協程的語法和框架的使用到這就已經全部介紹完了,剩下關於實際應用,請關注後面文章的更新。
更多協程和框架的介紹,可以參考
https://github.com/Kotlin/kotlinx.coroutines
https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html
未完,請關注後面文章更新…