# kotlin channel 入门

kotlin channel 入门

前言

最近项目中对 kotlin 的使用比较多。不得不说 kotlin 确实可以极大的提高 android 的开发效率,有许多之前得用 java 写非常多、非常啰嗦的样板代码的 case,用 kotlin 却可以几行搞定,四两拨千斤,同时逻辑表达也更加清晰。而 kotlin 对于 java 而言,最大的不同莫过于协程了。习惯了 kotlin 的协程,可能再也不想使用 java 的 handler + postDelay 了。因此,在这里,本人准备对 kotlin 协程中一些比较难以上手的点,进行说明和分析。这篇文章,将会带大家一起学习一下 kotlin 协程中 channel 的使用。

channel 概述

kotlin 中,我们常用 defer 来进行协助之间单个值的传递。比如,我们可能会写如下代码:

val deferred = GlobalScope.async {
  // do something,
  "this is a result"
}
deferred.await()

用来等待一个异步协程的结果。在结果返回之前,当前协程挂起。那么,如果我们想获取一系列的结果,应该怎么办呢?注意,这里的一系列的结果,不是说我们需要一个 list,而是说,我们想第一次 await() 的时候,得到一个值,然后再次 await() 的时候,还能获取到值。就像从一个队列里面不断的取出新的元素一样。

这个时候我们就可以使用 channel 了。channel 非常类似于一个 java 中非常常见的概念 BlockingQueue 。只不过,BlockingQueue 使用可以阻塞的 put 方法,而 channel 使用可以挂起的 send 方法;BlockingQueue 使用可以阻塞的 take 方法,而 channel 使用可以挂起的 receive 方法。所以,如果什么时候我们对于 channel 的理解产生了困惑,可以简单的把相关的内容类比到 BlockingQueue 中,来帮助我们进行理解。

channel 的用法

val channel = Channel<Int>()
launch {
    for (x in 1..5) channel.send(x * x)
}
repeat(5) { println(channel.receive()) }
println("Done!")

简单说明一下上面的代码:我们有一个 channel ,我们会从这个 channel 中 receive 5 次。这五次一次获取到从 1 到 5 一共五个数字。

这个简单的代码片段,其实蕴含了非常重要的程序执行流程:

我们假设,根据代码的书写顺序,先执行到了 channel.send(1) 。根据上面阐述的内容,send 因为是一个挂起的方法,第一次只会执行 1,并把 1 放入到 channel 中。然后,receive 方法获取到 1。这个时候在 repeat(5) 的循环中,再次执行到 receive 的时候,因为 channel 中已经没有数了,所以 receive 会挂起。之后,协程会通过调度算法,让 channel.send(2 * 2) 执行,并让 channel.send(3 * 3) 挂起。再之后,channel.receive() 在经过调度之后,得到执行,获取到刚才 channel.send(2 * 2) 的结果,也就是 4 。以此类推。

  1. channel.send(1)
  2. 发送方挂起
  3. channel.receive(1)
  4. 接收方挂起
  5. channel.send(4)
  6. 发送方挂起
  7. channel.receive(4)
  8. 接收方挂起

。。。

channel 的关闭和遍历

channelqueue 的一个不同的点就是,channel 是可以关闭的。close 这个动作,底层其实是给 channel 发送了一个消息。官方管这个东西叫 close token。因为 channel 在接收到 close 消息的时候,会立刻停止在这个 channel 上的遍历的工作,所以 kotlin 会保证在 close 被调用之前已经在 channel 中的消息被 received

kotlin 为我们提供了一个简单的 channel 的遍历方法,也就是 for 循环,使用方法如下:

val channel = Channel<Int>()
launch {
    for (x in 1..5) channel.send(x * x)
    channel.close() // we're done sending
}
// here we print received values using `for` loop (until the channel is closed)
for (y in channel) println(y)
println("Done!")

channel 的流水线模式

流水线模式的使用场景如下:一个协程不断的生产新的消息,其他协程不断的处理这些消息,并且在这个过程中可能返回新的结果。跟我们说的函数式编程中的 map(映射) 非常类似。

这个模式可以让我们很轻松的写出一些简洁而逻辑清晰的代码,比如,下面代码展示了如何生成素数的逻辑:

fun CoroutineScope.numbersFrom(start: Int) = produce<Int> {
    var x = start
    while (true) send(x++) // infinite stream of integers from start
}

fun CoroutineScope.filter(numbers: ReceiveChannel<Int>, prime: Int) = produce<Int> {
    for (x in numbers) if (x % prime != 0) send(x)
}

fun main() {
  var cur = numbersFrom(2)
    repeat(10) {
    val prime = cur.receive()
    println(prime)
    cur = filter(cur, prime)
    }
    coroutineContext.cancelChildren() // cancel all children to let main finish
}

这样,执行 main() 方法之后,就会输出前 10 个素数。这里的原理也很简单。如果我们不考虑 kotlin 的语法问题,但从计算机的角度解决生成素数的问题,一种解法是,我们需要用一个 list 来存储已经找到的素数,然后,在对 n 进行自增的过程中,遍历所有已经找到的素数 list,如果所有的素数都不能整除 n,那么这个 n 就是新的素数。

pipeline 模式写出的代码,原理跟上面阐述的一样。只是上文所说的 list 被封装在了一层一层的 filter 中,最终执行的过程中,对于一个 n ,需要通过所有的 filter,这跟上文说的遍历所有已经找到的素数列表的效果是一致的。

numbersFrom(2) -> filter(2) -> filter(3) -> filter(5) -> filter(7) ... 

扇入和扇出

扇入:多对一,多个 channel 作为生产者,一个 channel 作为消费者。

扇出:一对多,一个 channel 作为生产者,多个 channel 作为消费者。

虽然概念有不同,但是,写法上,跟一对一的 channel 是一样的。

缓冲 channel

channel 默认的 capacity 是 1。这也就是我们上文说的,send 方法在第二次会挂起,因为中间没有 receive 来消费这个消息。直到有 receive 消费了上一个消息之后,刚才挂起的 send 才能恢复执行。当然,我们可以通过设置参数让这个 capacity 的值不为 1,比如4。那么,跟上面的分析是一样的,send 会执行四次,然后在第五次的时候挂起,直到有 receive 把消息给消费掉了之后,之前挂起的 send 才能继续恢复执行。

channel 的公平性

channel 是公平的。所以,他会严格的按照 first-in first-out 的顺序来执行。一个比较好的例子,就是模拟打乒乓球:

data class Ball(var hits: Int)

fun main() = runBlocking {
    val table = Channel<Ball>() // a shared table
    launch { player("ping", table) }
    launch { player("pong", table) }
    table.send(Ball(0)) // serve the ball
    delay(1000) // delay 1 second
    coroutineContext.cancelChildren() // game over, cancel them
}

suspend fun player(name: String, table: Channel<Ball>) {
    for (ball in table) { // receive the ball in a loop
        ball.hits++
        println("$name $ball")
        delay(300) // wait a bit
        table.send(ball) // send the ball back
    }
}

// prints
ping Ball(hits=1)
pong Ball(hits=2)
ping Ball(hits=3)
pong Ball(hits=4)

这里的公平性就体现在,ping 协程是先启动的,所以理应获得到 ball。但是 ping 在通过调用 sendball 送还给 channel 之后,又在循环的下一轮立刻请求获取 ball。但是,因为 pongreceive 是比 ping 的下一个 receive 先调用的,(是在上一个 ping 的后面调用的),所以是 pong 得到 ball 而不是 ping

Ticker channels

channel 还有一种比较常用的用法,就是用来实现令牌系统。比如,我们现在的需求,是每 100 ms 产生一个令牌,那么我在 51ms 来取,肯定是获取不到的。但是我在 101ms 的时候来取,是可以获取到的。考虑一种情况,令牌没有得到及时的消费,比如,就是前 150ms 都没有消费,那么第 151ms 来的消费者是可以立刻获取到令牌的。但是,第 152ms 来的消费者是不能获取到令牌的。但是,第 201ms 过来的消费者是可以获取到令牌的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 这是第一次主动写简书,也是时隔多年再次提笔(上一次大概是高中作文?大学小论文?总之是别人要求的思想),这次是主动的...
    对立人阅读 1,102评论 3 3
  • 许久许久之前,我在和朋友吹牛扯淡时信誓旦旦地表示: 在未来,微信一定会取消发表纯文字朋友圈内容的功能。 依据如下:...
    云寒阅读 1,133评论 0 2
  • NO.1 人与人之间的关系往往不是因为某些具体的原因而断绝。不,即使表面上有某种原因,其实是因为彼此的心已经不在一...
    聚字成书阅读 511评论 0 1
  • 敬爱的李老师,智慧的班主任,亲爱的跃友们: 大家好!我是来自文登奥沃斯教育的王春叶,是黄栎媛的人,今天是我日精进行...
    王春叶阅读 124评论 0 0