ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Kotlin 协程核心概念完全指南:从入门到结构化并发

Kotlin 协程核心概念完全指南:从入门到结构化并发 一、前言为什么协程是 Kotlin 的杀手锏Java 时代写并发要么用 Thread重、阻塞、难管要么用 RxJava学习曲线陡、操作符复杂。Kotlin 协程用看似同步、实则异步的写法把并发代码写得和顺序代码一样自然——这就是它的杀手锏。本文从协程的本质出发串起挂起函数、作用域、调度器、结构化并发四大核心概念配上 20 个可直接运行的小例子。二、协程是什么不是线程是轻量级协程2.1 协程 vs 线程维度线程 (Thread)协程 (Coroutine)由谁管理OS 内核用户态Kotlin 运行时切换开销~1-10 μs系统调用~100 ns用户态内存占用~1 MB / 个几十字节 / 个并发数几百就 OOM轻松 10 万通信方式共享内存 锁通道Channel协程跑在线程上。一个线程可以跑成千上万个协程协程间协作式调度。2.2 第一个协程importkotlinx.coroutines.*funmain()runBlocking{launch{delay(1000L)// 挂起 1 秒非阻塞println(World!)}println(Hello,)}输出Hello, World!runBlocking启动一个阻塞主线程的协程用于 main 函数和测试。launch启动一个不阻塞的子协程。三、挂起函数suspend协程的灵魂3.1 suspend 关键字suspendfunfetchUser():User{delay(1000L)// 挂起点不阻塞线程returnUser(Alice)}suspend函数只能在协程里或另一个suspend函数里调用。编译器会自动生成状态机挂起时不阻塞线程恢复时自动接续。3.2 状态机原理简化suspendfunexample(){println(A)delay(1000L)// 挂起点 1println(B)delay(1000L)// 挂起点 2println(C)}编译器生成的状态机伪代码classExampleSM{int label0;Object result;voidresume(){switch(label){case0:println(A);label1;delay(1000L,this);// 异步等待挂起return;case1:println(B);label2;delay(1000L,this);return;case2:println(C);label-1;return;}}}挂起时不阻塞线程线程可以去执行其他协程。3.3 自定义挂起函数suspendfungetToken():String{returnwithContext(Dispatchers.IO){// 模拟网络请求delay(500L)token-123}}withContext切换调度器并挂起完成后自动恢复。四、协程构建器launch / async / runBlocking4.1 三大构建器对比构建器返回值用途异常传播launchJob“火并忘”不返回结果抛到父作用域asyncDeferredT“异步取值”返回结果通过await()抛runBlockingT阻塞主线程桥接阻塞代码直接抛4.2 launch 例子funmain()runBlocking{valjob:Joblaunch{delay(1000L)println(Launched!)}println(Done)job.join()// 等待子协程println(After join)}4.3 async 例子funmain()runBlocking{valdeferred1:DeferredIntasync{compute(1)}valdeferred2:DeferredIntasync{compute(2)}valsumdeferred1.await()deferred2.await()println(Sum $sum)// 123}suspendfuncompute(x:Int):Int{delay(500L)returnx*2}性能对比上面代码如果用 Thread 跑两次串行耗时 1s但async并发执行同样 1s不是 500ms因为 compute 内部 delay 是串行的如果 compute 是 CPU 密集型并行能省一半时间。4.4 runBlocking 何时用main函数单元测试桥接 Java 阻塞代码生产代码里基本不用 runBlocking——会阻塞线程违背协程初衷。五、调度器Dispatcher协程跑在哪个线程5.1 5 个内置调度器调度器线程池用途Dispatchers.Main主线程UI 更新、ViewModel 业务逻辑Dispatchers.IO64 个线程默认网络、数据库、文件 I/ODispatchers.DefaultCPU 核数CPU 密集型排序、解析Dispatchers.Unconfined当前线程高级场景一般不用newSingleThreadContext(name)自定义串行任务5.2 切换调度器suspendfunloadAvatar():BitmapwithContext(Dispatchers.IO){valbytesURL(https://example.com/avatar.png).readBytes()BitmapFactory.decodeByteArray(bytes,0,bytes.size)}调用loadAvatar()时在调用方所在线程执行withContext内部切到 IO 线程池结束后自动切回原线程。5.3 性能陷阱// ❌ 错误UI 线程做网络launch(Dispatchers.Main){valdataURL(https://api.example.com).readText()// 卡 UItextView.textdata}// ✅ 正确launch(Dispatchers.Main){valdatawithContext(Dispatchers.IO){URL(https://api.example.com).readText()}textView.textdata}六、作用域CoroutineScope协程的生命周期6.1 作用域是什么作用域是协程的容器决定了协程的父子关系用于结构化并发协程的取消传播协程的异常传播6.2 三大内置作用域作用域生命周期典型用途GlobalScope进程级慎用容易内存泄漏lifecycleScopeActivity/Fragment 生命周期Android UIviewModelScopeViewModel 生命周期Android ViewModelcoroutineScope { }块级结构化并发业务逻辑组合6.3 协程取消valjoblaunch{repeat(1000){i-println(Job:$i)delay(500L)}}delay(1300L)// 1.3 秒后取消job.cancel()job.join()println(main: 结束)挂起函数自动响应取消delay、yield等但普通计算不会// ❌ 取消不了launch{vari0while(iInt.MAX_VALUE){i// 死循环}}// ✅ 正确做法检查 isActivelaunch{vari0while(isActiveiInt.MAX_VALUE){i}}或用ensureActive()launch{try{while(true){ensureActive()// 抛 CancellationException// 计算}}catch(e:CancellationException){println(被取消)}}6.4 作用域父子关系runBlocking{launch{// 子协程 1delay(1000L)println(1 完成)}launch{// 子协程 2delay(2000L)println(2 完成)}println(等所有子协程)// runBlocking 会等所有子协程完成后才退出}这是结构化并发父协程自动等所有子协程完成子协程抛异常会传到父。七、结构化并发Structured Concurrency7.1 三大原则生命周期绑定协程不能比它的作用域活得更长自动取消父作用域取消所有子协程跟着取消自动等待父作用域结束前等所有子协程7.2 coroutineScope 与 supervisorScopesuspendfunloadData()coroutineScope{valaasync{fetchA()}// 失败会取消 bvalbasync{fetchB()}a.await()b.await()}函数异常处理coroutineScope { }任一子协程失败 → 其他全部取消supervisorScope { }子协程失败互不影响适合 UI 组件7.3 异常传播规则场景launch 异常async 异常根作用域立即抛到线程 Uncaught通过await()抛子协程传给父父取消同 launch// ❌ launch 异常会拖垮父runBlocking{launch{throwRuntimeException(Oops)}delay(1000L)println(这里不会执行)// 父被取消}// ✅ async 异常可隔离runBlocking{valdeferredasync{throwRuntimeException(Oops)}try{deferred.await()}catch(e:Exception){println(捕获:$e)}println(继续)// 正常执行}7.4 异常处理三大件// 1. try-catchlaunch{try{riskyCall()}catch(e:Exception){handle(e)}}// 2. CoroutineExceptionHandlervalhandlerCoroutineExceptionHandler{_,e-println(捕获:$e)}runBlocking{launch(handler){throwRuntimeException(Oops)}}// 3. SupervisorJob 局部处理supervisorScope{launch{/* 失败不影响其他 */}launch{/* 失败不影响其他 */}}八、Channel协程间的管道8.1 生产者-消费者模式importkotlinx.coroutines.channels.*funmain()runBlocking{valchannelChannelInt()// 生产者launch{for(xin1..5){channel.send(x)}channel.close()// 关闭}// 消费者for(yinchannel){println(收到:$y)}}输出收到: 1 收到: 2 收到: 3 收到: 4 收到: 58.2 4 种 Channel 类型类型容量行为ChannelRENDEZVOUS0send 必须有 receive 等默认Channel.UNLIMITED无限send 永不阻塞Channel.CONFLATED1只保留最新值Channel.BUFFERED64默认缓冲kotlinx-coroutines 1.38.3 produce / actor 构建器// 生产者协程funCoroutineScope.produceNumbers()produceInt{varx0while(true){send(x)delay(100L)}}// 使用runBlocking{valnumbersproduceNumbers()repeat(5){println(numbers.receive())}numbers.cancel()}九、Flow异步数据流RxJava 替代品9.1 第一个 Flowfunnumbers():FlowIntflow{for(iin1..3){delay(100L)emit(i)}}funmain()runBlocking{numbers().collect{value-println(value)}}9.2 冷流 vs 热流类型特点代表冷流Cold每个收集者都触发一次执行flow { }热流Hot多个收集者共享同一执行StateFlow,SharedFlow9.3 常用操作符flow{emit(1);emit(2);emit(3);emit(4)}.map{it*2}// 转换.filter{it2}// 过滤.take(2)// 取前 N 个.collect{println(it)}// 收集// 输出: 4, 69.4 StateFlow / SharedFlow替代 LiveDataclassMyViewModel:ViewModel(){privateval_stateMutableStateFlow(0)valstate:StateFlowInt_state.asStateFlow()funincrement(){_state.value// 自动去重相同值}}十、Android 中的最佳实践10.1 ViewModel 里用 viewModelScopeclassUserViewModel:ViewModel(){privateval_userMutableStateFlowUser?(null)valuser_user.asStateFlow()funloadUser(id:String){viewModelScope.launch{_user.valuewithContext(Dispatchers.IO){api.getUser(id)}}}}10.2 Activity 里用 lifecycleScopeclassMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)lifecycleScope.launch{repeatOnLifecycle(Lifecycle.State.STARTED){viewModel.user.collect{user-updateUI(user)}}}}}repeatOnLifecycle防止后台时更新 UI 导致崩溃。10.3 千万别用 GlobalScope// ❌ 内存泄漏 Activity 销毁后仍在跑GlobalScope.launch{...}// ✅ 总是用绑定生命周期的 scopelifecycleScope.launch{...}viewModelScope.launch{...}十一、面试高频问答速记Q1协程和线程的区别A协程是用户态轻量级线程跑在线程之上由 Kotlin 运行时调度切换开销 ~100ns vs 线程 ~1-10μs。Q2suspend 函数和普通函数的区别Asuspend 函数可以挂起编译器生成状态机只能在协程或 suspend 函数里调用。Q3launch 和 async 的区别Alaunch 不返回结果async 返回 Deferred 可以通过 await() 拿结果。Q4什么是结构化并发A父协程等子协程完成子协程异常传播给父作用域销毁自动取消所有子协程。Q5coroutineScope 和 supervisorScope 的区别AcoroutineScope 任一子失败则全部取消supervisorScope 子失败互不影响。Q6Flow 和 Channel 的区别AChannel 是事件流单次消费Flow 是数据序列可多次 collect。Q7协程如何取消A调用 job.cancel()挂起函数delay/yield会抛 CancellationException但纯计算需要检查 isActive。十二、总结速查表协程本质用户态协程跑在线程上 挂起函数suspend 关键字 编译器生成状态机 构建器launch不返回值 / async返 Deferred / runBlocking桥接 调度器MainUI/ IO64线程 I/O / DefaultCPU 作用域GlobalScope慎用 / lifecycleScope / viewModelScope / coroutineScope 结构化并发父等子、父子同生死、异常向上传播 通信Channel点对点 / Flow异步序列 / StateFlow状态写在最后协程的精髓不是语法而是结构化并发这个理念——它强制你思考协程什么时候开始、什么时候结束、谁负责它避免了大量并发 bug。建议把上面的 20 个例子挨个跑一遍光看不动手永远学不会协程。
返回列表