启动协程:launch与async的区别、runBlocking的使用场景、GlobalScope的坑

协程怎么启动?说白了就三种方式:launchasyncrunBlocking。还有一个GlobalScope,嗯,这个我建议你尽量别碰。

我记得刚学协程那会儿,看到这么多启动方式,头都大了。后来踩了几个坑,才慢慢摸清楚它们的脾气。今天咱们就把这几个东西彻底聊透。

launch vs async:一个点火,一个问路

先看个最简单的例子:

// launch:只管启动,不关心结果
fun main() = runBlocking {
    val job = launch {
        delay(1000L)
        println("任务完成")
    }
    // job 是一个 Job 对象,可以用来等待或取消
    job.join()
}

// async:启动并期待返回值
fun main() = runBlocking {
    val deferred = async {
        delay(1000L)
        "计算结果"
    }
    // deferred 是一个 Deferred<String> 对象
    val result = deferred.await()
    println(result) // 输出:计算结果
}

看出区别了吗?launch就像你点了个火,烧完就完了。async呢,像是派了个人去办事,办完了还得回来跟你汇报结果。

核心区别一句话:

  • launch 返回 Job,不关心返回值
  • async 返回 Deferred<T>,通过 await() 获取结果

我在项目中遇到过这样一个场景:同时请求三个接口,等全部返回后再做处理。用async就特别合适:

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        val three = async { doSomethingUsefulThree() }
        
        // 三个任务并发执行,全部完成后汇总
        println("结果:${one.await() + two.await() + three.await()}")
    }
    println("耗时:$time ms")
}

你想想看,如果换成launch,你得自己维护一堆变量来存结果,多麻烦。

runBlocking:桥接两个世界

runBlocking是个特殊的存在。它能把协程世界和普通代码世界连接起来。

fun main() {
    println("开始")
    
    runBlocking {
        delay(1000L)
        println("协程内执行")
    }
    
    println("结束")
}

运行结果:

开始
(等待1秒)
协程内执行
结束

注意看,runBlocking会阻塞当前线程,直到里面的协程全部执行完。这跟launch完全不一样——launch是非阻塞的。

⚠️ 使用场景要谨慎

runBlocking主要用在两个地方:

  • main函数里,作为协程的入口
  • 测试代码中,方便写单元测试

千万别在Android的主线程里用runBlocking,那会导致ANR。我曾经见过有人这么写,结果APP直接卡死……

GlobalScope:看起来方便,用起来坑

先看个例子:

fun main() {
    GlobalScope.launch {
        delay(1000L)
        println("GlobalScope 执行")
    }
    
    println("main 结束")
    // 程序直接退出了,协程里的代码根本没执行
}

为什么会这样?因为GlobalScope启动的协程生命周期跟整个应用绑定。main函数执行完了,JVM就退出了,协程还没来得及跑呢。

更坑的是什么呢?

GlobalScope 的三大问题:

  1. 生命周期不可控:协程跟应用同生共死,你没法精确控制它什么时候结束
  2. 容易造成内存泄漏:如果协程里持有Activity的引用,Activity销毁了协程还在跑
  3. 浪费资源:协程会一直占用线程池,即使没人关心它的结果

我曾经接手过一个项目,里面大量使用GlobalScope。结果呢?页面退出了,网络请求还在跑,回调回来时Activity已经没了,各种空指针异常。那段时间修bug修到怀疑人生……

💡 我的建议:

能用CoroutineScope就别用GlobalScope。在Android里,用lifecycleScopeviewModelScope,它们会自动管理生命周期。

实战对比:选哪个?

场景 推荐方式 原因
触发一次操作,不关心结果 launch 比如日志上传、埋点
需要并发执行并获取结果 async 比如多个接口并行请求
main函数或测试 runBlocking 桥接协程和普通代码
全局任务 自定义CoroutineScope 别用GlobalScope

最后说一句:launchasync没有谁好谁坏,关键看你要什么。要结果就用async,不要结果就用launch。就这么简单。

嗯,关于启动协程的几种方式,今天就聊到这儿。下一节咱们聊聊协程的取消与超时,那个坑更多,到时候我慢慢跟你说。