协程是Kotlin带来的一项明星功能。通过使用比线程更加轻量的协程,程序的性能得到了极大的提高。但是协程的运行控制有与传统的线程不尽相同,尤其是suspend函数的引入,更加使协程的使用令人迷惑。本文试图通过使用更加简单的方式对如何使用Kotlin协程进行简述。
需要注意的是,虽然Kotlin Coroutines声称其是使用的协程,但是在功能的底层实现上,依旧还是多线程,只是多线程的调度和交互已经被Kotlin Coroutines做了极大的优化。
协程的控制
suspend函数是发挥协程的性能威力,妥善处理异步的核心。不同于Go语言中的协程,Kotlin所提供的协程实际上是一套“线程框架”,其对任务的处理方式更加类似于Java中的Executor。所以对于协程的控制,实际上是可以借用线程的控制的。
在整个操作系统的概念中,是没有协程这个概念的,我们所有程序的代码都是在线程中运行的,也就是说如果没有使用多线程API专门启动其他的线程,我们的程序是以单线程的方式运行的。而线程又是依附于进程的,在一个进程中,可以存在众多的线程。协程的概念首先是Go语言提出来的,Go中的协程是一个比线程更加轻量的结构,一个线程中可以轻松的运行若干个协程,而且这个协程的结构是根植与Go语言的核心中的。
但是Kotlin首先是一种JVM语言,不像Go语言那样没有历史包袱。Kotlin所提供的协程,是一种可以在一个线程中运行并完成调度,然后将一些比较耗时或者需要等待的放到其他的线程中去运行,这样就可以让运行核心程序的主线程能够不被“阻塞”。所以协程的特点就是可以使用同步代码的形式写出异步的程序。
不同于Java中使用回调函数来处理异步,协程利用“挂起”来处理异步。suspend函数在整个协程中标记了所有耗时和需要在其他线程中处理的任务,每当协程遇到suspend函数的时候,就会把函数的执行放到其他的线程中去执行,然后将当前的协程挂起,这样程序的主线程就可以去执行其他的协程了。所以Kotlin协程的调度方法可以参考以下示意图。
所以,一个协程的“挂起”实际上就是让这个协程从当前的线程上脱离,去了调度器给它指定的线程去并行运行了。但是等到这个协程运行结束以后,Kotlin协程框架还会自动的把它切回来,这个操作就像是协程在之前的线程上被唤醒了一样。
协程的启动
在Kotlin程序中启动一个协程,可以使用launch
、async
、runBlocking
。
|
|
使用launch
和async
的时候,需要指定协程的作用域,例如上例中的GlobalScope
即表示程序的全局作用域,在这个作用域中是可以启动后台协程的。一个比较常见的是直接调用launch
和async
,这样的话新协程将会继承其父级协程的上下文,变成一个子协程。
启动协程的这三个函数的区别如下:
launch
,启动一个协程,但并不关心其内部的返回结果。async
,启动一个协程,可以从其中返回结果。协程的运行结果可以使用.await()
获取。runBlocking
,启动一个协程,但是这个协程将会阻塞启动它的线程。通常会在单元测试中用到。
Dispatchers
启动协程的函数launch、async还可以接受Dispatcher参数,用于指示即将启动的协程要如何调度,常用的调度有以下几种,并且会随着项目引入的其他依赖出现新的调度器。
Dispatchers.Main
,程序的主线程。Dispatchers.IO
,针对磁盘和网络优化的IO线程。Dispatchers.Default
,适用于CPU密集型任务的线程。Dispatchers.Swing
,Swing进行UI渲染的线程。Dispatchers.JavaFx
,JavaFx进行UI渲染的线程。
suspend函数
其实描述到这里,suspend函数已经没有什么秘密了,suspend
关键字实际上不执行任何挂起协程的功能,它存在的唯一意义,就是提醒开发者,这个函数是一个异步函数,需要被放到协程中去执行。所以一个suspend函数在书写起来就跟普通函数没有什么两样,例如:
|
|
但是一般suspend函数最好还是使用withContext
指示一下这个suspend函数需要使用哪种调度器,例如下面这个示例:
|
|
合成示例
综合以上示例,可以组成一个附带有后台协程和IO协程以及密集计算协程的示例。
|
|