Android开发:Kotlin协程并发模型(人话版)
一、核心基础:协程的并发与并行
1. 核心前提:并发 ≠ 并行
并发:单核CPU场景下,任务快速切换(毫秒/微秒级),看起来像同时执行,本质是「轮流执行」(如单线程内的协程、单线程多任务)。
并行:多核CPU场景下,多个任务在不同核心上物理同时执行,本质是「真正同时跑」。
2. 协程的并发与并行逻辑
单线程内的协程:仅能实现「并发」,依赖协程主动让出CPU(如await、IO等待),切换速度比线程快100~1000倍(无内核切换开销)。
协程的「并行」:无法单独实现,必须依托「多线程/多进程 + 多核CPU」—— 协程挂靠在操作系统线程上,多个线程分配到不同CPU核心,实现协程并行。
关键结论:同一时刻,真正在CPU上运行的协程数量 ≤ 活跃的操作系统线程数量(协程必须挂靠线程执行,一个线程同一时刻只能执行一个协程)。
二、协程运行机制(结合实战代码)
1. 实战代码解析(IO并行请求)
suspendfunloadData():CombinedData=coroutineScope{valuserDeferred=async(Dispatchers.IO){api.getUser()}valnewsDeferred=async(Dispatchers.IO){api.getNews()}CombinedData(userDeferred.await(),newsDeferred.await())}2. 逐步运行逻辑
进入coroutineScope:创建父协程,父协程运行在调用它的线程(如安卓主线程)。
启动async协程:两个async分别创建子协程,指定Dispatchers.IO,提交到IO线程池,线程池分配空闲线程(如Thread-IO1、Thread-IO2)执行网络请求;async不等待,直接返回Deferred对象,代码继续执行。
执行await():父协程调用await()的瞬间,会「挂起」并「立刻释放当前占用的线程」(无需等待所有await或所有子协程完成)。
协程恢复:子协程执行完成(网络请求结束),父协程重新获取线程,继续执行后续代码,组装并返回CombinedData。
3. 关键细节:await()与线程释放
任意一个协程(父协程/子协程)调用await()(或任何挂起函数),调用者协程会立即挂起,同时释放当前占用的线程。
线程释放与协程作用域(coroutineScope)无关:coroutineScope仅负责等待所有子协程完成,不管理线程释放;线程释放由「协程挂起」决定。
恢复后线程的归属:若协程绑定Dispatchers.Main(主线程),恢复后必回主线程;若绑定Dispatchers.IO/Default(线程池),恢复后线程可能变化(随机分配线程池中的空闲线程)。
三、协程调度器详解(Dispatchers)
1. 两大核心调度器(Kotlin官方规则)
| 调度器 | 适用场景 | 最大线程数规则 | 核心特点 |
|---|---|---|---|
| Dispatchers.Default | CPU密集型任务 | 等于CPU核心数(如8核=8个,10核=10个) | 线程全程占用CPU,多开线程会增加切换开销,降低效率 |
| Dispatchers.IO | IO密集型任务 | max(64, CPU核心数)(绝大多数设备=64) | 线程99%时间在等待(如网络、文件读写),不占CPU,多开线程提升并发效率 |
2. 关键补充
CPU密集型任务:全程纯计算、不等待,如图片压缩、JSON解析、加密解密、复杂算法运算(必须用Default)。
IO密集型任务:主要时间在等待,如网络请求、文件读写、数据库操作(必须用IO)。
IO线程上限64的原因:64个线程足够支撑移动端极高并发,再多会浪费内存(线程栈占用)和增加内核切换开销,是行业通用最优值。
四、常见疑问答疑(高频踩坑点)
1. 疑问:启动100个async(IO调度器),线程池不够用会怎样?
解答:不会崩溃、不卡顿、不报错。IO线程池默认上限64,剩余36个协程会进入轻量级等待队列,等有空闲线程(IO线程完成等待、释放线程)后,自动取出执行;排队的协程无内存开销,不占用CPU。
2. 疑问:8核手机IO线程上限64,10核是不是80?
解答:不是。IO线程上限遵循max(64, CPU核心数),只要CPU核心数≤64,无论8核、10核、32核,上限都是64;只有CPU核心数>64(如服务器65核),上限才等于核心数。
3. 疑问:父协程中多个await,需要所有await都调用才释放线程吗?
解答:不需要。只要执行任意一个await(),调用它的协程就会立即挂起、释放线程;后续await()会再次挂起、释放线程,直到所有子协程完成,协程恢复。
4. 疑问:协程挂起(await)后,恢复时一定能回到原来的线程吗?
解答:不一定,看调度器:绑定Dispatchers.Main(主线程),恢复后必回主线程;绑定Dispatchers.IO/Default(线程池),恢复后可能切换到线程池中的其他空闲线程。
五、核心总结(必记)
协程:用户态轻量级任务,依赖线程执行,无线程无法运行;核心优势是「用户态快速切换」和「挂起不阻塞线程」。
并行:协程需依托多线程+多核CPU,同一时刻运行的协程数≤线程数;并发:单线程内协程快速切换,无需多核。
调度器选择:CPU密集用Default(线程数=核心数),IO密集用IO(上限64),主线程操作绑定Main。
await():调用即挂起、释放线程,恢复线程由调度器决定;coroutineScope仅负责等待子协程,不管理线程。
