从进程到协程【深度解析】——必懂的并发编程

作为开发者,我们每天都在和 “并发” 打交道 ——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)

  1. 协程 A 执行,遇到 await(IO 操作)。

  2. 协程 A 将控制权 yield(让出)给调度器,并告诉调度器:“等这个 IO 好了叫我”。

  3. 调度器查看任务队列,发现协程 B 可以运行,于是切换到 B 执行。

  4. 当 IO 完成,操作系统通知调度器。

  5. 调度器在合适的时机将协程 A 放回执行队列,从上次暂停的地方继续执行。

协程中的关键概念

协程上下文(CoroutineContext)

协程上下文是协程的运行环境配置,本质是一组元素的集合,定义了协程的运行规则。

核心元素:

  • Job:协程的唯一标识,用于控制协程的生命周期(启动、取消、等待);
  • CoroutineDispatcher:调度器,决定协程运行在哪个线程;
  • CoroutineExceptionHandler:异常处理器;
  • CoroutineName:协程名称,用于调试;
  • ThreadContextElement:线程上下文元素(如日志 MDC、事务上下文)。
协程作用域(CoroutineScope)

协程作用域是协程的生命周期边界,本质 = CoroutineContext + Job,是启动协程的容器,负责定义协程的生命周期、管理作用域内所有协程的层级关系、提供launch(启动协程) asycn(启动带返回值的协程)等方法。

常用作用域

作用域绑定生命周期适用场景
lifecycleScopeActivity/Fragment页面级协程(如 UI 交互、单次请求)
viewModelScopeViewModel数据层协程(如数据缓存、跨页面数据)
GlobalScope应用生命周期不推荐(无结构化并发,易泄漏)
runBlocking阻塞当前线程仅测试场景使用
协程调度器(CoroutineDispatcher)

协程调度器决定协程运行在哪个线程 / 线程池

常用调度器

调度器作用客户端场景
Dispatchers.Main主线程(UI 线程)更新 UI、处理用户交互
Dispatchers.IOIO 密集型线程池网络请求、文件 IO、数据库操作
Dispatchers.DefaultCPU 密集型线程池大数据计算、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 可以捕获作用域内所有未处理的异常。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值