协程入门:协程的概念与优势、GlobalScope与runBlocking、suspend函数、launch与async、Job与Deferred

各位同学,欢迎来到协程的世界。说实话,我刚开始接触Android开发那会儿,处理异步任务全靠回调嵌套回调,代码写出来跟个金字塔似的,维护起来简直想哭。后来Kotlin协程出现了,我第一次用的时候心里还在嘀咕:这玩意儿不就是个线程池封装吗?结果用了一个星期,真香。

协程,说白了就是一套帮你管理异步任务的框架。它不是线程,但跑在线程上。你可以把它理解成一种轻量级的线程——轻量到什么程度?一个线程能跑成千上万个协程,而且切换成本极低。嗯,这里要注意,协程本身并不等于并发,它只是让异步代码写起来像同步代码一样自然。

核心要点:协程是Kotlin提供的异步编程解决方案,它让回调地狱变成了顺序代码。你想想看,以前写网络请求要回调里套回调,现在用协程,直接一行suspend函数搞定。

协程的优势

我在项目中遇到过不少场景,比如同时请求三个接口,等全部返回后再更新UI。用传统方式你得写CountDownLatch或者RxJava的zip操作符,代码量不小。用协程呢?一个async就解决了。

  • 轻量级:一个协程就是一个状态机,不占用额外线程资源。我测试过,开10万个协程毫无压力,换成线程早OOM了。
  • 结构化并发:协程有父子关系,父协程取消,子协程自动取消。这比手动管理线程安全多了。
  • 代码可读性高:用协程写的异步代码,读起来就像同步代码一样。没有回调,没有嵌套。
  • 异常处理友好:try-catch就能捕获协程里的异常,不用像RxJava那样写一堆onError。
协程核心概念关系图 Kotlin 协程 GlobalScope 全局作用域 runBlocking 阻塞当前线程 launch 返回Job async 返回Deferred suspend 函数:协程中的挂起函数,可被其他suspend函数调用 Job:协程的句柄,用于控制协程的生命周期(取消、等待完成) Deferred:带返回值的Job,通过await()获取结果

GlobalScope 与 runBlocking

先说说GlobalScope。这玩意儿我建议你少用,甚至别用。为什么?因为它是一个全局作用域,协程的生命周期跟整个应用绑定。你在Activity里用GlobalScope启动一个协程,Activity销毁了协程还在跑,这就容易出内存泄漏。我曾经在一个老项目里看到到处都是GlobalScope.launch,结果内存泄漏查了半天,最后全改成lifecycleScope了。

// 不推荐:GlobalScope 启动的协程生命周期与应用一致
GlobalScope.launch {
    delay(1000)
    println("这个协程可能泄漏")
}

// 推荐:使用 lifecycleScope 或 viewModelScope
lifecycleScope.launch {
    delay(1000)
    println("安全,Activity销毁协程自动取消")
}

再来看runBlocking。这名字起得很直白——它会阻塞当前线程直到协程执行完毕。一般用在main函数或者测试代码里。你想想看,在Android主线程里用runBlocking?那画面太美我不敢看,直接ANR警告。

// 通常在 main 函数或测试中使用
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World")
    }
    println("Hello")
    // 输出顺序:Hello -> (1秒后) World
}

警告:千万不要在Android主线程中使用runBlocking!它会阻塞UI线程,导致应用无响应。我曾经见过有人这么写,结果被测试小姐姐追着骂了一下午。

suspend 函数

suspend是协程的核心关键字。一个函数加上suspend,就变成了可挂起的函数。什么叫挂起?说白了就是协程执行到这个函数时,可以暂停下来,等函数执行完再继续往下走。而且这个暂停不会阻塞线程,线程可以去干别的事。

我个人习惯把suspend函数想象成一个带暂停按钮的播放器。你点暂停,播放器停了,但电源没断。等再点播放,从暂停的地方继续放。协程的挂起也是这个道理。

// suspend 函数示例
suspend fun fetchUserData(): User {
    // 模拟网络请求
    delay(2000) // delay 本身就是一个 suspend 函数
    return User("张三", 25)
}

// 调用 suspend 函数必须在协程作用域内
lifecycleScope.launch {
    val user = fetchUserData() // 这里会挂起2秒,但不会阻塞主线程
    textView.text = user.name
}

小技巧:suspend函数只能在协程作用域内或者其他suspend函数中调用。如果你在普通函数里调用suspend函数,编译器会报错。嗯,这个坑我刚开始也踩过。

launch 与 async

launch和async是启动协程的两种方式。launch返回一个Job,不关心返回值;async返回一个Deferred,可以通过await()获取返回值。怎么选?很简单:需要返回值就用async,不需要就用launch。

// launch:不关心返回值
lifecycleScope.launch {
    // 执行一些操作,不需要返回结果
    updateUI()
}

// async:需要返回值
lifecycleScope.launch {
    val deferred = async {
        fetchDataFromNetwork()
    }
    val result = deferred.await() // 等待结果
    showResult(result)
}

我在项目中经常用async做并行请求。比如同时请求用户信息和商品列表,两个接口没有依赖关系,就可以并行执行,等两个都返回后再一起处理。

lifecycleScope.launch {
    val userDeferred = async { apiService.getUserInfo() }
    val productDeferred = async { apiService.getProductList() }
    
    // 两个请求并行执行,这里等待两个都完成
    val user = userDeferred.await()
    val products = productDeferred.await()
    
    // 更新UI
    showUserInfo(user)
    showProducts(products)
}

Job 与 Deferred

Job是协程的句柄,你可以通过它来控制协程的生命周期。比如取消协程、等待协程完成。Deferred是Job的子接口,多了一个await()方法用来获取返回值。

特性 Job Deferred
返回值 有(通过await获取)
启动方式 launch async
取消 job.cancel() deferred.cancel()
等待完成 job.join() deferred.await()
// Job 的使用示例
lifecycleScope.launch {
    val job = launch {
        repeat(10) {
            delay(500)
            println("执行第 $it 次")
        }
    }
    
    delay(2000)
    job.cancel() // 2秒后取消协程
    println("协程已取消")
}

// Deferred 的使用示例
lifecycleScope.launch {
    val deferred = async {
        delay(1000)
        "Hello from async"
    }
    
    val result = deferred.await() // 等待并获取结果
    println(result) // 输出:Hello from async
}

重要提醒:Job和Deferred都支持结构化并发。如果父协程被取消,所有子协程也会被自动取消。这个特性在Android开发中特别有用,比如页面销毁时自动取消所有网络请求。

好了,协程入门的内容就这些。说实话,协程刚出来的时候我也觉得有点绕,但用习惯了真的回不去。你想想看,以前写异步代码要各种回调、线程切换,现在一个suspend函数加一个launch就搞定了。下一节我们会深入协程的调度器与上下文,到时候再聊更多实战技巧。


公众号:蓝海资料掘金营,微信deep3321