Kotlin-协程(二)启动协程

282 阅读3分钟

这一篇主要讲解协程的三种启动方式。

一、launch 启动协程

我们先看看源码中launch函数是如何定义的:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

launchCoroutineScope的扩展函数,而CoroutineScope是一个接口,它的实现类有很多,常见的有:GlobeScopeLifecycleCoroutineScopeviewModelScope(ViewModel扩展)等,用最简单的GlobeScope(不要用在生产环境)启动一个协程:

fun main() {
    GlobalScope.launch {
        printMsg("start")
        delay(100)
        printMsg("end")
    }
    Thread.sleep(2000L)
}

fun printMsg(msg: String) {
    println("${LocalDateTime.now()} ${Thread.currentThread().name}:$msg")
}

//打印
2023-02-10T17:45:11.082989200 DefaultDispatcher-worker-1 @coroutine#1:start
2023-02-10T17:45:11.204989900 DefaultDispatcher-worker-1 @coroutine#1:end

线程名为DefaultDispatcher-worker-1,协程名为@coroutine#1,但会发现代码中有线程sleep的代码,为什么需要sleep呢? 假如去掉会怎么样? 试试看:

fun main() {
    GlobalScope.launch {
        printMsg("start")
        delay(100)
        printMsg("end")
    }
}

//打印

那为什么开启的协程没有线程阻塞(sleep)就不执行了呢? 那是因为通过 launch 创建的协程还没来得及开始执行,整个程序就已经结束了,阻塞线程就是为了让线程不那么快退出。

我们看launch的返回值不是协程内执行的结果,launch 为什么无法将结果返回给调用方呢?我们看上面launch 函数的源代码,你就会发现,这个函数的返回值是一个 Job,它其实代表的是协程的句柄(Handle),它并不能为我们返回协程的执行结果。

二、runBlocking 启动协程

runBlocking 跟我们前面学的 launch 的行为模式不太一样,通过它的名字,我们就可以看出来,它是存在某种阻塞行为的。让我们将前面 launch 的代码直接改为 runBlocking,看看运行结果是否有差异:

fun main() {
   runBlocking{
        printMsg("start")
        delay(2000)
        printMsg("end")
    }
}

fun printMsg(msg: String) {
    println("${LocalDateTime.now()} ${Thread.currentThread().name}:$msg")
}

//打印
2023-02-16 09:12:00.855448200 main @coroutine#1:start
2023-02-16 09:12:02.873297400 main @coroutine#1:end   //间隔2秒

即使不加sleep的代码,所有的日志会且按顺序打印,这是因为runBlocking 会阻塞当前线程的执行,。对于这一点,Kotlin 官方也强调了:runBlocking 只推荐用于连接线程与协程,并且,大部分情况下,都只应该用于编写Demo 或是测试代码,所以,请不要在生产环境当中使用 runBlocking。

虽然runBlocking阻塞了线程,但如果在runBlocking中启动其他协程,执行结果也不会完全按顺序执行的,比如:

fun main() {
    runBlocking {
        printMsg("runBlocking start")
        launch {
            printMsg("launch start")
            delay(2000L)
            printMsg("launch end")
        }
        printMsg("runBlocking end")
    }
}

//打印
2023-02-16T09:51:21.818149200 main @coroutine#1:runBlocking start
2023-02-16T09:51:21.823149600 main @coroutine#1:runBlocking end
2023-02-16T09:51:21.826148900 main @coroutine#2:launch start  //协程在runBlocking end后面输出
2023-02-16T09:51:23.843212700 main @coroutine#2:launch end

另外,你可以注意到它的第二个参数suspend CoroutineScope.() -> T,这个函数类型是有返回值类型 T 的,而它刚好跟 runBlocking 的返回值类型是一样的。因此,我们可以推测,runBlocking 其实是可以从协程当中返回执行结果的。看下面的示例:

fun main() {
    val result = runBlocking<String> {
        printMsg("start")
        delay(2000)
        printMsg("end")
        return@runBlocking "Hello World"  //添加runBlocking协程作用域的返回值
    }
    printMsg(result)
}

fun printMsg(msg: String) {
    println("${LocalDateTime.now()} ${Thread.currentThread().name}:$msg")
}

//打印
2023-02-16T09:39:33.127854800 main @coroutine#1:start
2023-02-16T09:39:35.141483700 main @coroutine#1:end
2023-02-16T09:39:35.143483600 main:Hello World

所以,从表面上看,runBlocking 是对 launch 的一种补充,但由于它是阻塞式的,因此,runBlocking 并不适用于实际的工作当中。那么,还有什么办法可以让我们拿到协程当中的执行结果吗?答案就是:async

三、async 启动协程

使用 async{} 创建协程,并且还能通过它返回的句柄拿到协程的执行结果。让我们看个简单的例子:

fun main() {
    runBlocking {
        printMsg("runBlocking start")
        val asyncDeferred: Deferred<String> = async {
            printMsg("launch start")
            delay(2000L)
            printMsg("launch end")
            return@async "Hello World"
        }

        //获取asyncDeferred的结果
        val result = asyncDeferred.await() //重点在这里
        printMsg("result:$result")  //拿到了结果
        printMsg("runBlocking end")
    }
}

//打印
2023-02-16T09:59:01.415796100 main @coroutine#1:runBlocking start  //外部协程执行
2023-02-16T09:59:01.429796100 main @coroutine#2:launch start  //内部协程执行
2023-02-16T09:59:03.449260100 main @coroutine#2:launch end    //内部协程结束
2023-02-16T09:59:03.453258700 main @coroutine#1:result:Hello World  //外部协程输出结果
2023-02-16T09:59:03.453258700 main @coroutine#1:runBlocking end   //外部协程结束

为什么等待内部协程执行完才执行外部的协程代码呢? 有二个细节:
① await会让外部协程挂起

image.png

② await会等待调用者的协程执行完才会恢复外部协程
await的源码如下:

/**
 * Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete,
 * returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
 *
 * This suspending function is cancellable.
 * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
 * immediately resumes with [CancellationException].
 * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
 * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
 *
 * This function can be used in [select] invocation with [onAwait] clause.
 * Use [isCompleted] to check for completion of this deferred value without waiting.
 */
public suspend fun await(): T

注释中有说道,await函数没有阻塞线程,只是等待deferred执行完成就恢复(resumes),而deferred正是async的返回值。

参考内容

如何启动协程

个人学习笔记