作为开发者,我们每天都在和 “并发” 打交道 ——APP 里的网络请求要异步执行、后台下载文件不能阻塞 UI、多任务同时处理要避免卡顿…… 而理解进程、线程、协程的本质及关系,是搞定客户端并发编程的核心基础。尤其是协程,如今已成为 Android(Kotlin)、iOS(Swift 5.5+)、跨平台(Flutter/Dart)开发的标配,掌握它能让你彻底摆脱 “回调地狱”,写出更简洁、高效的代码。
进程和线程
进程:程序运行的 “独立容器”
本质
进程的本质是操作系统分配资源的基本单位,可以理解为 “一个正在运行的程序实例”。每个安装在手机上的 APP,启动后都会被操作系统创建一个独立的进程(也可配置多进程),比如你打开微信,系统会为微信分配内存、CPU 时间片、文件句柄等资源,这些资源完全归微信进程独占,和其他 APP 进程隔离。
核心特性
- 独立地址空间:不同进程的内存空间相互隔离,一个进程崩溃(比如闪退)不会影响其他进程(除非是系统级进程)。比如微信闪退,不会导致支付宝也关闭。
- 资源独占性:进程拥有自己的堆、栈、文件描述符等,操作系统通过进程 ID(PID)唯一标识。
- 进程间通信(IPC)成本高:由于隔离性,进程间交换数据需要借助操作系统提供的特殊机制,客户端开发中常见的有:
- Android:Binder(四大组件通信核心)、AIDL、ContentProvider;
- iOS:XPC、Mach 端口、Socket;
- 通用方式:共享内存、消息队列、Socket。
线程:进程的 “执行单元”,CPU 调度的最小单位
本质
线程本质上是CPU可调度的最小执行单元,隶属于进程,是代码的真正执行者。一个进程至少包含一个线程(主线程 / UI线程)。客户端开发有一个铁律:UI 操作必须在主线程(UI 线程)执行,耗时操作必须在子线程执行。
核心特性
- 轻量级:线程共享所属进程的所有资源(内存、文件句柄等),创建和销毁的开销远低于进程;
- 抢占式调度:由操作系统内核调度,CPU 会给每个线程分配时间片,线程在时间片内执行,时间片结束后切换到其他线程(上下文切换);
- 线程安全问题:多个线程共享进程资源时,若同时操作同一数据,会出现 “竞态条件”(比如两个线程同时修改一个计数变量)、死锁等问题。
线程模型的痛点
- 上下文切换开销:内核切换线程时需要保存 / 恢复线程状态,高并发下开销显著
- 资源限制:手机端能创建的线程数有限(一般几千级),过多线程会导致调度效率下降;使用线程执行大量IO也会“撑爆线程池”
- 回调地狱:多线程异步操作嵌套时,代码会变得杂乱(比如 “网络请求→解析数据→更新 UI” 的多层回调)
- 调度不可控:什么时候切换线程由操作系统决定;切换伴随着寄存器、PC、栈的保存 / 恢复,不适合大量轻量任务
协程
协程是一种比线程更轻量的用户态并发模型,官方描述为用户态的轻量级线程,其调度完全在用户态,不需要操作系统切换上下文。
协程的工作原理
协程的魔法在于“挂起”(Suspend)和“恢复”(Resume)。
想象你在煮面(主任务),同时想烧水(IO任务):
传统同步阻塞: 打开水壶 -> 傻站在旁边等水开(CPU 空转) -> 水开了 -> 去煮面。
多线程: 雇两个人,一个人盯着水壶,一个人煮面(资源浪费)。
协程:
-
你去打开水壶(发起 IO)。
-
你对自己说:“水烧开需要时间,我先去切葱花(挂起当前烧水任务,切换到切葱花任务)。”
-
水壶响了(IO 完成)。
-
你放下手里的葱花,回来处理开水(恢复烧水任务上下文)
内部实现原理(简化版)
在代码层面,通常包含一个 调度器 (Event Loop)。
-
协程 A 执行,遇到
await(IO 操作)。 -
协程 A 将控制权
yield(让出)给调度器,并告诉调度器:“等这个 IO 好了叫我”。 -
调度器查看任务队列,发现协程 B 可以运行,于是切换到 B 执行。
-
当 IO 完成,操作系统通知调度器。
-
调度器在合适的时机将协程 A 放回执行队列,从上次暂停的地方继续执行。
协程中的关键概念
协程上下文(CoroutineContext)
协程上下文是协程的运行环境配置,本质是一组元素的集合,定义了协程的运行规则。
核心元素:
Job:协程的唯一标识,用于控制协程的生命周期(启动、取消、等待);CoroutineDispatcher:调度器,决定协程运行在哪个线程;CoroutineExceptionHandler:异常处理器;CoroutineName:协程名称,用于调试;ThreadContextElement:线程上下文元素(如日志 MDC、事务上下文)。
协程作用域(CoroutineScope)
协程作用域是协程的生命周期边界,本质 = CoroutineContext + Job,是启动协程的容器,负责定义协程的生命周期、管理作用域内所有协程的层级关系、提供launch(启动协程) asycn(启动带返回值的协程)等方法。
常用作用域
| 作用域 | 绑定生命周期 | 适用场景 |
|---|---|---|
lifecycleScope | Activity/Fragment | 页面级协程(如 UI 交互、单次请求) |
viewModelScope | ViewModel | 数据层协程(如数据缓存、跨页面数据) |
GlobalScope | 应用生命周期 | 不推荐(无结构化并发,易泄漏) |
runBlocking | 阻塞当前线程 | 仅测试场景使用 |
协程调度器(CoroutineDispatcher)
协程调度器决定协程运行在哪个线程 / 线程池
常用调度器
| 调度器 | 作用 | 客户端场景 |
|---|---|---|
Dispatchers.Main | 主线程(UI 线程) | 更新 UI、处理用户交互 |
Dispatchers.IO | IO 密集型线程池 | 网络请求、文件 IO、数据库操作 |
Dispatchers.Default | CPU 密集型线程池 | 大数据计算、JSON 解析、图片处理 |
Dispatchers.Unconfined | 无指定线程 | 极少使用(易导致线程切换混乱) |
线程切换
withContext是协程中最常用的线程切换方法,会挂起当前协程,在指定调度器执行代码后返回结果,自动切回原调度器:
lifecycleScope.launch(Dispatchers.Main) {
// 当前在主线程
val data = withContext(Dispatchers.IO) {
// 切换到IO线程执行
fetchData()
}
// 自动切回主线程,更新UI
tvContent.text = data
}
核心特性
非阻塞:协程通过其特有的挂起机制实现非阻塞,当遇到 I/O操作时(例如网络请求),它会暂停自身执行,将底层的物理线程立即让出给其他可以运行的协程。线程从未阻塞,而是持续工作。
顺序化表达异步逻辑:协程解决了传统异步编程中的”回调地狱“问题,允许开发者使用类似于同步代码的线性、自上而下的风格来编写复杂的异步流程(通过 await 或 suspend 关键字)。
结构化并发:协程的生命周期必须与其所在的程序结构(即协程作用域)绑定,任务不再是独立漂浮的,而是形成了清晰的父子层级关系,有效防止内存泄漏。
协作式取消:协程的取消是协作的,而非抢占的,它不会粗暴地强行终止协程,只有当协程执行到挂起点时,才会检查取消状态。
统一异常处理模型:依赖于结构化并发,异常会沿着Job层级树清晰地向上传播,子协程的未处理异常会传播到父协程,父协程取消时会取消所有子协程;协程内部也可以使用 try-catch 捕获异常,通过 CoroutineExceptionHandler 可以捕获作用域内所有未处理的异常。
137

被折叠的 条评论
为什么被折叠?



