Kotlin Coroutine —— 一、基本概念

362 阅读5分钟

1. 你的第一个协程

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

Hello,
World!

GlobalScope.launch 是一个位于某个 CoroutineScope 中的 coroutine builder,我们通过该方法在 GlobalScope 中启动一个新的协程。GlobalScope 是在生命周期方面和 Java Thread 类似的一个执行环境,也就说 GlobalScope 的生命周期将不受调用函数的作用域、实例生命周期的限制,如果它的执行时间足够长,那么 GlobalScope 将在应用的整个生命周期中占用资源。

2. 连接阻塞和非阻塞两个世界

在我们上面的例子中,有两个「延时」的方法: delayThread.sleep。但是 delay 是一个「非阻塞」方法, Thread.sleep 是一个「阻塞」方法,混合使用只会让人很难分清哪一个在「阻塞」线程,而哪一个没有。我们可以将其统一成「非阻塞」方法来达到「延时」的目的:

fun main() { 
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

上面的代码能达到同样的效果,但是使用了 runBlocking 来显式地表示这是一个「阻塞」的方法。 runBlocking 会一直「阻塞」直到它其中的子协程全部都依次执行完(非子协程部分会并行于子协程执行)。

我们可以再改写下上面的代码,使其开起来更加协程化:

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

上面的 runBlocking<Unit> 中可以显示地指明泛型。该泛型表示协程的返回类型,由于 delay 的返回类型就是 Unit,因此在上面的例子中, runBlocking 的泛型是没有必要的(可以通过最后一行的 delay 推断出来)。

3. Waiting for a Job

我们上面的代码是通过时间来控制两次打印(两次任务)的执行先后顺序,事实上我们应该让两个任务无缝连接地先后执行。

fun main() = runBlocking {
    val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes    
}

每个启动的协程块都会返回一个 Job 对象使调用者可以控制该协程。

3. 结构化的并发

虽然每个协程块都会返回一个 Job 对象,但是如果我们显式地通过持有 Job 去管理协程,当有多个协程时将会非常难以管理。

我们有更好的方法:使用结构化并发。前面的代码我们很难管理的一个原因是,我们在 GlobalScope 中启动的协程,它的生命周期默认是跟应用程序的生命周期一样的,但是事实上,我们可以在我们自己的作用域中去启动协程。

每一个 coroutine builder,包括 runBlocking ,都会向它的代码块提供一个 CoroutineScope 的实例。在作用域中启动的子协程不用显式地调用 join 来同步与其父协程的顺序关系,因为父协程会等待所有由它启动的子协程都结束后才会结束。所以,我们可以将例子代码这样写:

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine in the scope of runBlocking
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

4. Scope builder

除了 runBlockingGlobalScope.launch 这样的协程构造器中它们自带的 coroutine scope 外,你还可以通过调用 coroutineScope 构造器来申明你自己的 coroutine scopecoroutineScope 会创建一个新的 coroutine scope ,同其他所有的 coroutine scope 一样,只有它的子协程全部都结束后,它才会结束。

runBlockingcoroutineScope 看起来差不多,但是它们之间有个最主要的区别,就是 runBlocking 会阻塞它的调用者,而 coroutineScope 只会将它的调用者线程挂起。正是这个区别,因此 runBlocking 是一个普通函数,而 coroutineScope 则是一个挂起函数。

4.1 runBlocking, coroutineScope, launch 之间的区别

首先我们定义一个函数用于计算函数调用的时刻。

private fun timeDelta(startTimestamp: Long) = (System.currentTimeMillis() - startTimestamp)

我们先看下 runBlockinglaunch 之间的特性:

// 代码清单 scope-builder-1
fun main() = runBlocking<Unit> { // this: CoroutineScope
    val startTimestamp = System.currentTimeMillis()
    launch {
        val delay = 200L
        delay(delay)
        println("${timeDelta(startTimestamp)}: runBlocking 中的任务 1 ($delay)")
    }

    launch {
        val delay = 50L
        delay(delay)
        println("${timeDelta(startTimestamp)}: runBlocking 中的任务 2 ($delay)")
    }
    println("${timeDelta(startTimestamp)}: runBlocking 执行完毕") // This line is not printed until the nested launch completes
}

5: runBlocking 执行完毕
70: runBlocking 中的任务 2 (50)
214: runBlocking 中的任务 1 (200)

我们可以发现,任务1 的开始时间 214 并没有在 任务2 的结束时间 70 上累加(如果累加的话, 任务1 的开始时间应为 284),因此 launch 之间其实是并发地执行。我们再看看 runBlockingcoroutineScope 之间的关系:

// 代码清单 scope-builder-2
fun main() = runBlocking<Unit> { // this: CoroutineScope
    val startTimestamp = System.currentTimeMillis()

    coroutineScope {
        val delay = 150L
        delay(delay)
        println("${timeDelta(startTimestamp)}: 执行第一个 coroutineScope 里的协程($delay)")
    }

    println("${timeDelta(startTimestamp)}: 第一个 coroutineScope 结束")

    coroutineScope { // Creates a coroutine scope
        launch {
            val delay = 50L
            delay(delay)
            println("${timeDelta(startTimestamp)}: 执行第二个 coroutineScope 中的任务($delay)")
        }

        val delay = 10L
        delay(delay)
        println("${timeDelta(startTimestamp)}: 执行第二个 coroutineScope 里的协程($delay)") // This line will be printed before the nested launch
    }

    println("${timeDelta(startTimestamp)}: runBlocking 执行完毕") // This line is not printed until the nested launch completes
}

167: 执行第一个 coroutineScope 里的协程(150)
168: 第一个 coroutineScope 结束
182: 执行第二个 coroutineScope 里的协程(10)
222: 执行第二个 coroutineScope 中的任务(50)
223: runBlocking 执行完毕

可以看到 coroutineScope 之间是依次顺序执行的。

Q: 为什么 代码清单 scope-builder-1并发地执行,而 代码清单 scope-builder-2阻塞地执行呢?

A: 我们先看下 launch 的源码:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
		// 使用 EmptyCoroutineContext 构造新的 context
    val newContext = newCoroutineContext(context)
		// 使用新构造的 context 来构造一个 Coroutine 对象
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
		// 执行这个新的 Coroutine
    coroutine.start(start, coroutine, block)
    return coroutine
}

从上面我们可以看到,虽然 launch 构造了新的 CoroutineContext 和新的 Coroutine ,但是没有构造新的 Scope

再看下 coroutineScope 的源码:

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
				// 通过 ScopeCoroutine 创建了新的 Scope
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

我们可以看到 coroutineScope 通过 ScopeCoroutine 构造了新的 Scope

因此, 如果协程运行在新的 Scope 中,那么这些新的 Scope 之间会阻塞地执行。如果只是通过 launch 等形式运行协程,那么这些协程之间会并行执行

A: runBlockingcoroutineScope 的区别呢?

Q: runBlocking 的「阻塞」性质并不是指的「阻塞」子 Scope 使其顺序执行,而是指的它不是一个「挂起」函数。 coroutineScoperunBlocking 一样都会等待其子 Scope 顺序执行完,区别只是 runBlocking 是一个普通函数,会阻塞调用者线程,而 coroutineScope 是一个挂起函数,不会阻塞调用者线程:

fun main() = runBlocking<Unit> { // this: CoroutineScope
    val startTimestamp = System.currentTimeMillis()

    coroutineScope {
        val delay = 150L
        delay(delay)
        println("${timeDelta(startTimestamp)}: 执行第一个 coroutineScope 里的协程($delay)")

        coroutineScope {
            val delay1 = 200L
            delay(delay1)
            println("${timeDelta(startTimestamp)}: 执行第一个 coroutineScope 里的子 scope_1($delay1)")
        }

        coroutineScope {
            val delay2 = 100L
            delay(delay2)
            println("${timeDelta(startTimestamp)}: 执行第二个 coroutineScope 里的子 scope_2($delay2)")
        }
    }

    println("${timeDelta(startTimestamp)}: 第一个 coroutineScope 结束")

    println("${timeDelta(startTimestamp)}: runBlocking 执行完毕") // This line is not printed until the nested launch completes
}、

168: 执行第一个 coroutineScope 里的协程(150)
369: 执行第一个 coroutineScope 里的子 scope_1(200)
474: 执行第二个 coroutineScope 里的子 scope_2(100)
474: 第一个 coroutineScope 结束
474: runBlocking 执行完毕

可以看到 scope_1scope_2 之间是顺序执行的。