介绍协程
很长一段时间我都没有写博客了,主要是这段时间忙着毕业的事,一直无法专心于做某一件事,所以很多东西都耽搁了。今天聊一聊Kotlin的协程。
协程,又称微线程,纤程。英文名Coroutine。
上面一段是我摘自阮一峰老师的博客。我们知道最近几年协程这个概念很火,不少的语言都支持了协程,如Python,go等语言。当然由于很多语言历史局限性的存在,协程的使用并不友好,最好的当属go语言的协程,只需一个简单的go
关键字就可以开启协程之旅了。
今天就来说说kotlin的协程,kotlin在1.1版本后引入了协程,目前还处于实验性阶段。但是仍然可以拿来尝尝鲜,前面提到go语言的协程非常容易使用,在kotlin里面协程的使用也是非常简单的,而且还更加丰富。
使用协程
fun main(args: Array<String>) = runBlocking {
launch {
delay(1000L)
println("World")
}
}
复制代码
这段代码便可以在Kotlin中开启一个协程,与一般的主函数不同,由于协程的特殊性。Kotlin中使用协程的函数必须在前面加上suspend
关键字。suspend代表着当前函数可以被挂起
,从而将该函数的使用权交给调度器,调度器会在合适的时机去使用和调度该函数从而实现cpu资源的充分利用。
为了更方便的使用协程,kotlin的标准库为我们提供了runBlocking
这个函数。
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
复制代码
这个函数会默认的将{}
内部的语句挂载到suspend函数中,方便我们使用协程。Kotlin中提供了两个函数启动协程,分别是launch
和async
,这两个函数均可以启动协程,异同点我们将在后面叙述。
launch启动协程后,延时1s,输出World
。但是当真正运行这个函数时,我们其实得不到任何输出,原因很简单:
由于协程的非阻塞性,开启一段协程并不会阻塞当前的线程。协程在执行的时候,主函数仍在向下执行,可是主函数下没有任何执行语句,主函数便会退出,协程此时虽然仍在执行,但主函数已退出,整个程序就退出了。所以
World
这个词还没来得及输出,程序就已经退出了。
可是我们必须得知道协程是否真正的在执行,所以我们必须阻塞一下主函数,或者说让主函数不会立马退出,让协程有时间的去输出。所以这样改进我们的代码:
fun main(args: Array<String>) = runBlocking {
val job = launch {
delay(1000L)
println("World")
}
println("Hello, ")
Thread.sleep(2000L)
}
复制代码
如果一些顺利,我们会得到下面的输出:
Hello,
World
复制代码
当协程开启后,主程序与协程同时执行(这里的同时并不是指真真的同时,而是CPU调度速度快使我们感觉到二者在同时执行),但协程里面正在delay
,时间为1s,所以主程序先输出Hello,
,然后主程序正在被Thread.sleep(2000L)
所阻塞,时间为2s,这时候只被阻塞一秒的协程便有机会输出了,所以控制台后面输出World
,然后程序过一会儿便会自己退出。
这里可以看出,协程是真正的非阻塞式,它不会阻塞主程序的向下执行,而且它绝对收主程序控制,主程序退出,它也会被立刻退出。当然Thread.sleep(2000L)
这个函数并不地道,Kotlin提供了一个更好用的delay
函数给我们,方便我们进行协程的阻塞。所以,地道的程序如下所示:
fun main(args: Array<String>) = runBlocking {
val job = launch {
delay(1000L)
println("World")
}
println("Hello, ")
delay(2000L)
}
复制代码
如果一切正常,你将会得到同样的输出。但是这样的程序真的就地道吗,我们指定了delay
的时间,毫无疑问是不准确的,因此Kotlin为我们提供了一个更好的方法join
。它会阻塞主程序,直到协程执行完毕,所以真正地道的写法是这样的:
fun main(args: Array<String>) = runBlocking {
val job = launch {
delay(1000L)
println("World")
}
println("Hello, ")
job.join()
}
复制代码
这样,当协程在执行的时候,主程序等待,当协程完成自己的任务后,主程序继续执行。
当然协程最最最重要的特性——轻量。操作系统的进程是非常昂贵的,线程也是价值不菲的,但是协程真的是异常便宜,比如说:
fun main(args: Array<String>) = runBlocking {
val jobs = List(100000) {
launch {
delay(1000L)
print(".")
}
}
jobs.forEach { it.join() }
}
复制代码
我们开了多少了协程,没错10万个,但是计算机毫无感觉。可以想象,在高并发的情况下,协程带来了提升无疑是巨大的。
协程用于io
上面,我们很明显的感觉到了协程带来的优越性。但是更多的情况下,我们需要协程获取数据,比如说数据请求,而后得到该数据。launch
开启的协程,并不能完成这一点,它只能启动
协程,当然上面也提过Kotlin有两个函数可以开启协程。既然lanuch
不能,那async
是否可以了?答案是必须得,毕竟谁也不会没事搞两个函数。
Kotlin中的async
这个函数受到了C#影响,表明当前的函数的是个异步的函数,异步的函数必然会带有自己的callback
。但是为了使用方便和优美,Kotlin提供了await
方法,当数据请求完毕时,协程会自动调用该方法,并且返回请求的数据。我们通过下面的例子来看一下:
fun main(args: Array<String>) = runBlocking<Unit> {
val elasticDto = ElasticDto() ①
val job = async {
elasticDto.getLastestByPage() ②
}
job.await().also { ③
println(it.toString())
}
elasticDto.close() ④
}
复制代码
①:创建一个数据请求的类——elasticDto。
②:用async
开启一个协程,并在该函数下调用elasticDto的getLastestByPage
方法请求远端的数据,请求而来的数据会自动装入job
里面。
③:等待协程处理完毕,处理完毕后输出请求数据内容。
④:关闭elasticDto客户端。
与lanuch
不同,async
会返回一个Deferred
对象,该对象拥有协程完成后的回调功能,并从中取得请求来的数据。并且await
会如同前面的join
函数一样,阻塞当前的主程序。
当然Kotlin协程还实现了如Go语言一样的CSP
模型,通过Channel来实现协程之间的通信,笔者会在下篇比较Kotlin和Go语言在该方面的异同点。Kotlin协程实现了两种模式下通信方式,很大程度上既照顾了使用callback的方式,也拥抱了CSP模型。具体请参考官方的指北。