Kotlin协程启动方式详解

引言

协程(Coroutine)是Kotlin在异步编程领域的革命性解决方案。相比传统的线程和回调,协程通过挂起(suspend)而非阻塞的方式,让我们能够以同步代码的写法 实现异步任务的处理 。然而,很多初学者在刚接触协程时,往往被各种启动方式搞得晕头转向:launchasyncrunBlockingwithContext...它们之间有什么区别?分别在什么场景下使用?

本文将用最通俗的语言最直观的代码对比,帮你彻底搞懂Kotlin协程的启动方式和使用场景。

1. 前置准备

在开始之前,请确保项目中已经引入协程依赖:

复制代码
// build.gradle.kts (Module)
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

2. 协程构建器总览

Kotlin协程主要通过以下几种构建器启动:

构建器 返回值 特点 常用场景
launch Job 无返回值,即发即用 执行耗时任务,不关心结果
async Deferred<T> 有返回值,需要await()获取 并行任务,需要结果
runBlocking T 阻塞当前线程 桥接普通函数和挂起函数,测试
withContext T 切换协程上下文 指定代码块在特定线程执行

下面我们逐个击破。

3. launch:最常用的启动方式

launch是最基础的协程构建器,它返回一个Job对象,不包含任何结果值。

基本用法

复制代码
fun main() {
    // 需要在协程作用域中调用launch
    runBlocking {
        val job: Job = launch {
            delay(1000)
            println("任务执行中...")
        }
        println("launch不会阻塞后续代码")
        
        job.join() // 等待协程执行完毕
        println("任务结束")
    }
}

输出:

复制代码
launch不会阻塞后续代码
任务执行中...
任务结束

在Android中的真实场景

复制代码
class MainActivity : AppCompatActivity() {
    private val mainScope = MainScope()
    
    fun loadData() {
        // 在UI线程启动协程
        mainScope.launch {
            showLoading() // UI操作
            
            // 切换到IO线程执行网络请求
            val result = withContext(Dispatchers.IO) {
                api.fetchData() // 挂起函数,非阻塞
            }
            
            hideLoading() // UI操作
            showResult(result)
        }
    }
    
    override fun onDestroy() {
        super.onDestroy()
        mainScope.cancel() // 防止内存泄漏
    }
}

核心特点 :不关心返回值,类似于线程的Thread.start()

4. async:需要返回值时使用

当我们需要协程执行完毕后返回某个结果时,async是我们的不二选择。它返回Deferred<T>对象,通过await()获取结果。

基本对比

复制代码
fun main() = runBlocking {
    // ========== launch 版本 ==========
    var result1 = ""
    val job1 = launch {
        delay(1000)
        result1 = "数据1"
    }
    job1.join()
    println("launch结果: $result1")
    
    // ========== async 版本 ==========
    val deferred: Deferred<String> = async {
        delay(1000)
        return@async "数据1" // 或直接写 "数据1"
    }
    val result2 = deferred.await()
    println("async结果: $result2")
}

输出完全相同 ,但async的代码更加简洁优雅。

并行任务的王者

async的真正威力体现在同时执行多个独立任务

复制代码
fun main() = runBlocking {
    val time = measureTimeMillis {
        // 顺序执行 - 耗时2秒
        val result1 = getData1()
        val result2 = getData2()
        println("顺序结果: $result1 + $result2")
    }
    println("顺序耗时: ${time}ms")
    
    val time2 = measureTimeMillis {
        // 并行执行 - 耗时1秒
        val deferred1 = async { getData1() }
        val deferred2 = async { getData2() }
        
        val result1 = deferred1.await()
        val result2 = deferred2.await()
        println("并行结果: $result1 + $result2")
    }
    println("并行耗时: ${time2}ms")
}

suspend fun getData1(): String {
    delay(1000)
    return "数据1"
}

suspend fun getData2(): String {
    delay(1000)
    return "数据2"
}

输出:

复制代码
顺序结果: 数据1 + 数据2
顺序耗时: 2012ms
并行结果: 数据1 + 数据2
并行耗时: 1015ms

结论 :当任务相互独立时,使用async可以将总耗时从"串行之和"变为"最慢任务的耗时"。


5. runBlocking:桥梁与测试工具

runBlocking是一个特殊的构建器,它会阻塞当前线程直到内部协程执行完毕。

认识runBlocking

复制代码
fun main() {
    println("主线程开始: ${Thread.currentThread().name}")
    
    runBlocking {
        println("runBlocking进入: ${Thread.currentThread().name}")
        delay(1000)
        println("runBlocking执行中...")
    }
    
    println("主线程结束: ${Thread.currentThread().name}")
}

输出:

复制代码
主线程开始: main
runBlocking进入: main
runBlocking执行中...
主线程结束: main

看到问题了吗?runBlocking内部运行在调用它的线程 ,并且会阻塞该线程

为什么说它主要是测试工具?

复制代码
// ❌ 不推荐在正式代码中使用
class MainActivity : AppCompatActivity() {
    fun badExample() {
        // 这会阻塞UI线程!导致ANR
        runBlocking {
            delay(5000) // UI线程卡死5秒
        }
    }
}

// ✅ 只应该在测试或main函数中使用
@Test
fun testCoroutine() = runBlocking {
    val result = mySuspendFunction()
    assertEquals(expected, result)
}

6. withContext:优雅地切换线程

withContext不会创建新的协程,而是在当前协程内切换上下文(通常是切换线程),并返回结果

与async的对比

复制代码
fun main() = runBlocking {
    // ========== async + await ==========
    val result1 = async(Dispatchers.IO) {
        fetchUserInfo()
    }.await()
    
    // ========== withContext ==========
    val result2 = withContext(Dispatchers.IO) {
        fetchUserInfo()
    }
    
    // 完全等价!但withContext更简洁
    println("$result1, $result2")
}

suspend fun fetchUserInfo(): String {
    delay(1000)
    return "用户信息"
}

Android中的标准模板

复制代码
class UserViewModel : ViewModel() {
    private val _userLiveData = MutableLiveData<User>()
    val userLiveData: LiveData<User> = _userLiveData
    
    fun loadUser(userId: String) {
        viewModelScope.launch {
            _userLiveData.value = UserLoadingState // 显示加载中
            
            // withContext自动切换线程并返回结果
            val user = withContext(Dispatchers.IO) {
                repository.getUserFromNetwork(userId)
            }
            
            _userLiveData.value = user // 回到主线程更新UI
        }
    }
}

一句话总结withContext = 切换线程 + 同步返回结果


7. 完整对比:所有启动方式同台竞技

复制代码
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    println("========== 协程启动方式大对比 ==========\n")
    
    // 1. launch
    launch {
        delay(500)
        println("[launch] 无返回值,执行耗时任务")
    }
    
    // 2. async
    val deferred = async {
        delay(500)
        println("[async] 执行并返回结果")
        "我是返回值"
    }
    println("[async] 获取结果: ${deferred.await()}")
    
    // 3. withContext
    val result = withContext(Dispatchers.IO) {
        delay(500)
        println("[withContext] 切换线程并返回结果")
        "线程: ${Thread.currentThread().name}"
    }
    println("[withContext] 结果: $result")
    
    // 4. runBlocking 嵌套 - 不推荐
    runBlocking {
        delay(500)
        println("[runBlocking] 会阻塞外层协程的线程")
    }
    
    println("\n========== 并行执行能力对比 ==========")
    
    // 串行执行
    val time1 = measureTimeMillis {
        val user = getUser()
        val order = getOrder()
        println("串行结果: $user, $order")
    }
    
    // async并行
    val time2 = measureTimeMillis {
        val user = async { getUser() }
        val order = async { getOrder() }
        println("并行结果: ${user.await()}, ${order.await()}")
    }
    
    println("串行耗时: $time1 ms")
    println("并行耗时: $time2 ms")
}

suspend fun getUser(): String {
    delay(1000)
    return "张三"
}

suspend fun getOrder(): String {
    delay(1000)
    return "订单#10086"
}

8. 其他

launch和async的根本区别是什么?

launch返回Job,不包含结果;async返回Deferred<T>,包含可等待的结果。asynclaunch的"超集",增加了返回结果的能力。

如何选择使用launch还是async 以及withContext

  • 不需要结果launch(如日志上报、数据缓存)

  • 需要结果async(如网络请求、数据库查询)

  • 需要切换线程并返回结果withContext(Android开发中的最优解)

以下代码执行顺序是什么?

复制代码
runBlocking {
    println("1")
    launch {
        println("2")
        delay(1000)
        println("3")
    }
    println("4")
    async {
        println("5")
        delay(500)
        println("6")
    }.await()
    println("7")
}

答案:1, 4, 2, 5, 6, 7,3 , 也可能是1, 4, 5, 2, 6, 7 3

(思考:这个才是重点)

答案解释

后面的6, 7,3 可能大家都可能会理解。

很多人认为可能出现【 1, 2, 4, 5】,【1, 2, 5, 4】

主要是协程调度器的工作机制

  • launch 只是提交任务 ,不是执行任务

  • 提交任务 ≈ 把任务放到待执行队列

  • 执行任务 ≈ 从队列取出并运行

  • println("4") 在提交任务后立即执行,而此时任务还在队列中等待调度。

  • 所有的 launch 内部的代码都是在当前线程空闲后才执行的,而不是立即执行

因此首先是代码的执行顺序是

  1. 先println("1")

  2. 提交任务launch(叫他A协程)

  3. 执行下一条指令也就是 println(4)

  4. 提交任务async(叫他协程B)

  5. 执行的.await()方法, 线程空闲,触发调用协程

  6. 协程有2个,A,B都可能先执行,因此可能先 println("5"), 也可能先println("2")

这一步是理论上A,B谁都可能先执行,但是实际测试了100000次, 我的手机都是必定先执行打印2, 后打印5。 有知道细节的可以继续讨论一下。

  1. 后续输出6, 7 , 3

请重点记住,启动协程只是放到等待队列,等线程空闲时才执行

相关推荐
城东米粉儿1 小时前
Android EventHub的Epoll原理 笔记
android
gihigo19982 小时前
MATLAB运动估计基本算法详解
开发语言·算法·matlab
城东米粉儿2 小时前
Android音频系统 笔记
android
郝学胜-神的一滴2 小时前
TCP通讯的艺术:从握手到挥手的优雅对话
开发语言·网络·网络协议·tcp/ip·程序人生
黎雁·泠崖2 小时前
【魔法森林冒险】12/14 场景系统:5大场景的任务串联
java·开发语言
l1t2 小时前
在python 3.14 容器中安装和使用chdb包
开发语言·python·clickhouse·chdb
梵刹古音2 小时前
【C++】函数重写
开发语言·c++
民国二十三画生3 小时前
C++(兼容 C 语言) 的标准输入语法,用来读取一行文本
c语言·开发语言·c++
半切西瓜3 小时前
Android Studio 创建应用自动指定SDK目录
android·ide·android studio