[Kotlin] 关于协程的一点思考

什么是协程

我个人的理解,协程是一种可以让程序“挂起”的技术实现。“挂起”是指将当前程序运行的上下文信息全部保存起来,并可以在后续期间恢复整个现场的一种能力。
例如,Thread.sleep()

, object.wait(),就是一种“挂起”的实现,是利用java线程本身的特性实现的,最终依赖操作系统或者jvm的线程模型。
而协程不依赖操作系统或者虚拟机,在线程的基础上,通过一系列的原语(关键字)和利用编译器进行转换来实现“挂起”。

协程有什么用

协程最大的用处是可以把回调风格的逻辑(一般是处理并发),写成堵塞风格,而不会发生真正的线程堵塞(前提是要写得对)。

几个关键的概念

要理解协程,必须要在头脑里形成一个与具体实现无关的协程的模型。先记住几个关键的概念:Scope、CoroutineContext、suspend、resume,它们的作用分别是:

  • scope: 管理自身范围内的协程。
  • CoroutineContext:协程运行的上下文环境,保存着协程运行的所有状态。可嵌套
  • suspend:让程序挂起的操作。
  • resume:让程序恢复的操作。

通常情况下,一次使用协程的代码如下所示:

scope.createNewCoroutine(context) { context -> 
    ...
    result = doSomthingAsync() {
      ...
      context.resume
    }
    context.suspend
    ...
    result.get()
    ...
}

实际上无需显式地进行resume和suspend操作,整体看起来是堵塞式的。

如何使用协程

理解了上面的模型后,下面介绍一下如何使用kotlin的协程。注意,这里不展开解释kotlin的协程实现。

我们通过三个例子来初步认识协程:

eg1

log(1)
val job = GlobalScope.launch() {
    log(2)
}
job.join()
log(3)

启动协程前,我们需要一个scope,kotlin预置了众多scope。上面的例子中以GlobalScope为例,GlobalScope.launch()创建了一个协程的运行环境,在其范围内,suspend-resume的机制是有效的。代码里log(2)运行在协程的环境中,但是没有suspend操作。GlobalScope.launch()返回了一个Job对象,此对象类似于java的Thread,用于控制所对应的协程运行环境的生命周期。

eg2

log(1)
val job = GlobalScope.launch(Dispatcher.IO) {
    log(2)
}
job.join()
log(3)

上面的例子中,GlobalScope.launch()传入了Dispatcher.IO。Dispatcher.IO称之为调度器,实际上是一种CoroutineContext。Dispatcher.IO指定了其内部协程代码运行所在的线程为IO线程。注意,CoroutineContext由于重写了plus运算符,所以可以叠加(嵌套)。

eg3:

fun main(args: Array<String>) = runBlocking() {
    launch() {
        println("Coroutine start " + Thread.currentThread().hashCode())
        launch() {
            println("Child coroutine start " + Thread.currentThread().hashCode())
            delay(1000)
            println("Child coroutine end " + Thread.currentThread().hashCode())
        }
        println("Coroutine end  " + Thread.currentThread().hashCode())
    }
    println("Done " + Thread.currentThread().hashCode())
}

打印出:

Done 1851691492
Coroutine start 1851691492
Coroutine end  1851691492
Child coroutine start 1851691492
Child coroutine end 1851691492

上述例子中,中间通过launch关键字创建了一个新的子协程。delay(1000)调用时,内部进行了suspend操作(delay是一个suspend函数),所以"Coroutine end"会比"Child coroutine end"更早打印出来。值得注意,所有print语句都是在同一线程上进行的。这里可以明显看出与线程的区别:delay函数并没有堵塞线程,协程在delay处“挂起”了,1000毫秒后恢复。注意这里线程并没有“挂起”,“挂起”的是协程,就如进程和线程的关系,线程堵塞时进程一样在执行。这里协程“挂起”了,但是线程还在继续驱动着众多协程继续执行。

为什么要用协程?

通常情况下,堵塞式的写法在理解难度上总是比回调式的写法要低,但是在面向真实用户时,为了保证交互的流畅度(即不能堵塞UI线程),会采用回调式的写法去处理一系列的复杂逻辑。协程的出现,让我们有了在不堵塞UI线程的基础上,用堵塞式的写法实现业务逻辑。

协程与线程的关系,就像线程与进程的关系一样。使用线程,我们可以不堵塞进程。使用协程,我们可以不堵塞线程。