Kotlin 协程 (十三) ——— 多路复用

293 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、多路复用

多路复用是计算机通信中的概念,定义如下:

数据通信系统或计算机网络系统中,传输媒体的带宽或容量往往会大于传输单一信号的需求,为了有效地利用通信线路,希望一个信道同时传输多路信号,这就是所谓的多路复用技术。

这个定义或许有些难懂,知乎上有个非常通俗的例子解释多路复用:I/O多路复用技术(multiplexing)是什么

假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:

  1. 第一种选择:按顺序逐个检查,先检查 A,然后是 B,之后是 C、D。。。这中间如果有一个学生卡主,全班都会被耽误。

  2. 第二种选择:你创建 30 个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。

  3. 第三种选择,你站在讲台上等,谁解答完谁举手。这时 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() 函数可以获取到多个协程中,最先执行完成的协程返回的结果。