本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、多路复用
多路复用是计算机通信中的概念,定义如下:
数据通信系统或计算机网络系统中,传输媒体的带宽或容量往往会大于传输单一信号的需求,为了有效地利用通信线路,希望一个信道同时传输多路信号,这就是所谓的多路复用技术。
这个定义或许有些难懂,知乎上有个非常通俗的例子解释多路复用:I/O多路复用技术(multiplexing)是什么
假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
第一种选择:按顺序逐个检查,先检查 A,然后是 B,之后是 C、D。。。这中间如果有一个学生卡主,全班都会被耽误。
第二种选择:你创建 30 个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
第三种选择,你站在讲台上等,谁解答完谁举手。这时 C、D 举手,表示他们解答问题完毕,你下去依次检查 C、D 的答案,然后继续回到讲台上等。此时 E、A 又举手,然后去处理 E 和 A ... 这种就是 IO 复用模型,Linux 下的 select、poll 和 epoll 就是干这个的。在 tcp 服务器处理多个 socket 客户端时,只要将每个 socket 对应的 fd 注册进 epoll,然后 epoll 就会监听哪些 socket 上有消息到达,这样就避免了大量的无用操作。此时的 socket 应该采用非阻塞模式。
整个过程只在调用 select、poll、epoll 这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的 reactor 模式。
二、await() 多路复用
当我们使用 async() 开启协程时,可以通过 await() 函数获取执行结果。当有多个协程在等待 await() 结果时,我们可以通过 select() 函数监听所有协程,并获取到最先执行完的协程的结果。
runBlocking {
val deferred1 = async {
delay(1000)
"response1"
}
val deferred2 = async {
delay(2000)
"response2"
}
val result = select<String> {
deferred1.onAwait { it }
deferred2.onAwait { it }
}
println("result: $result")
}
在这个例子中,第一个协程等待 1s 后返回 "response1",第二个协程等待 2s 后返回 "response2",我们通过 select() 函数监听这两个协程,监听方式就是调用其 onAwait() 函数。
运行程序,输出如下:
result: response1
可以看到,由于 deferred1 会先执行完,所以 select() 函数获取到了 deferred1 的返回值。
这个功能在某些场景下会有用,比如开启多个协程从多个服务器请求同一个数据时,可以通过多路复用展示最先返回的结果。
类似于派多个工作人员去各个商店买同一件东西,当其中一个人买到之后,就直接返回。通过并发的方式提高了效率。
三、Channel 多路复用
Channel 的多路复用和 await() 的多路复用是类似的,当有多个 Channel 在发送消息时,可以通过 select() 函数监听最先发出来的消息:
runBlocking {
val channel1 = Channel<Int>()
val channel2 = Channel<Int>()
launch {
delay(1000)
if (!channel1.isClosedForSend) {
channel1.send(1)
}
}
launch {
delay(2000)
if (!channel2.isClosedForSend) {
channel2.send(2)
}
}
val result = select<Int> {
channel1.onReceive { it }
channel2.onReceive { it }
}
println("result: $result")
channel1.close()
channel2.close()
}
在这个例子中,channel1 等待 1s 后发射一条数据,channel2 等待 2s 后发射一条数据。然后通过 select() 函数来接收两个 Channel 中,先返回的数据。
运行程序,输出如下:
result: 1
四、判断函数是否可以被 select():SelectClause
如何知道哪些函数可以被用于 select() 呢?实际上,能够被 select() 的函数都是 SelectClauseN 类型,其中,N 表示这个函数有几个参数。
查看 Deferred 的 onAwait() 函数源码:
public val onAwait: SelectClause1<T>
Channel 的 onReceive() 函数源码:
public val onReceive: SelectClause1<E>
除此之外,onJoin() 方法也是可以被 select() 的:
onJoin: SelectClause0
使用示例:
runBlocking {
val job1 = launch {
delay(1000)
println("job1 done")
}
val job2 = launch {
delay(2000)
println("job2 done")
}
select<Unit> {
job1.onJoin {
println("select job1")
}
job2.onJoin {
println("select job2")
}
}
}
运行程序,输出如下:
job1 done
select job1
job2 done
所以,想要判断哪些函数可以被用于 select(),只要看其是否继承自 SelectClauseN 即可。
五、小结
本文我们介绍了多路复用的基础知识。使用 select() 函数可以获取到多个协程中,最先执行完成的协程返回的结果。