进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位;
线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位;
而协程是一种比线程更加轻量级的并发编程方式,它可以在一个线程中实现多个任务的并发执行。
维度 | 进程(Process) | 线程(Thread) | 协程(Coroutine) |
---|---|---|---|
定义 | 程序在操作系统中的一次执行实例,是资源分配的基本单位。 | 进程内的执行单元,是 CPU 调度的基本单位。 | 用户态轻量级 “线程”,由协程库管理,可在同一线程内协作式调度。 |
调度单位 | 操作系统内核调度 | 操作系统内核调度 | 协程库(用户态调度,无需内核参与) |
上下文切换 | 内核态切换,开销极大(涉及内存地址空间、文件描述符等)。 | 内核态切换,开销较大(涉及寄存器、栈指针等)。 | 用户态切换,开销极小(仅保存协程状态到堆,无需内核干预)。 |
内存占用 | 独立地址空间(通常数 MB 到 GB 级)。 | 共享进程内存空间,每个线程栈默认约 1 MB。 | 共享线程内存,每个协程仅需几个 KB(无独立栈,共享调用栈)。 |
并发性 | 进程间并发,由操作系统控制。 | 线程间并发,由操作系统控制(抢占式多任务)。 | 协程间并发,由协程库控制(协作式多任务,需主动挂起)。 |
创建开销 | 高(需分配独立内存、文件句柄等资源)。 | 中(需分配线程栈、寄存器上下文)。 | 极低(仅创建协程对象,复用线程资源)。 |
切换开销 | 最高(涉及内核态上下文和地址空间切换)。 | 较高(内核态线程上下文切换)。 | 最低(用户态协程状态保存 / 恢复,无内核参与)。 |
资源隔离 | 完全隔离(地址空间、文件描述符等)。 | 部分隔离(共享进程内存,独立栈、寄存器)。 | 不隔离(共享线程内存,通过协程作用域管理生命周期)。 |
执行控制权 | 操作系统完全控制(不可预测抢占)。 | 操作系统完全控制(不可预测抢占)。 | 协程主动控制(通过 suspend 挂起,协作式恢复)。 |
典型用途 | 独立程序运行(如浏览器、IDE 等独立进程)。 | 多任务处理(如网络请求、文件读写等异步操作)。 | 高并发轻量任务(如海量 I/O 操作、事件驱动逻辑)。 |
代表技术 | Linux 进程、Windows 进程。 | Java 线程、POSIX 线程(pthread)。 | Kotlin 协程、Go Goroutine、Python asyncio 协程。 |
生命周期 | 由操作系统管理(创建 / 销毁开销大)。 | 由操作系统管理(依赖进程生命周期)。 | 由协程库管理(绑定作用域,如 Android 的 lifecycleScope )。 |
上下文保存位置 | 硬盘或内存(进程切换时保存完整状态)。 | 内存(线程栈和寄存器状态)。 | 堆(协程状态封装为 Continuation 对象)。 |
阻塞影响 | 进程阻塞不影响其他进程。 | 线程阻塞会占用 CPU 时间片,影响同进程内其他线程。 | 协程阻塞不阻塞线程,可释放线程执行其他协程。 |
调度层级:
资源开销:
控制方式:
suspend
主动挂起,协作式恢复,适合细粒度异步控制。适用场景:
suspend
关键字在 Kotlin 协程中,suspend
关键字用于修饰函数,表明这个函数是一个挂起函数。挂起函数只能在协程内部或者另一个挂起函数中被调用。当调用挂起函数时,协程会暂停执行,直到挂起函数的操作完成,之后再恢复协程的执行。
从源码层面来看,Kotlin 编译器会对挂起函数进行特殊处理,将其转换为带有状态机的代码。下面结合代码示例深入讲解:
// 使用 suspend 关键字修饰函数,表明这是一个挂起函数
suspend fun getDataFromNetwork(): String {
// delay 是 Kotlin 协程库提供的一个挂起函数,用于模拟耗时操作,这里暂停 1000 毫秒
delay(1000)
return "Data from network"
}
// 在协程作用域中启动一个新的协程
GlobalScope.launch {
// 调用挂起函数,协程会在此处挂起,等待 getDataFromNetwork 函数执行完成
val result = getDataFromNetwork()
// 当挂起函数执行完成,协程恢复执行,打印结果
println(result)
}
在编译时,getDataFromNetwork
函数会被转换为一个状态机。以下是简化的状态机代码示例,用于说明其工作原理:
// 定义一个密封类来表示状态机的不同状态
sealed class GetDataFromNetworkState {
// 初始状态
object Initial : GetDataFromNetworkState()
// 等待延迟完成的状态
object WaitingForDelay : GetDataFromNetworkState()
// 任务完成的状态
object Finished : GetDataFromNetworkState()
}
// 模拟编译器转换后的挂起函数
fun getDataFromNetwork(state: GetDataFromNetworkState = GetDataFromNetworkState.Initial): Any? {
var currentState = state
while (true) {
when (currentState) {
// 初始状态,开始执行挂起函数
is GetDataFromNetworkState.Initial -> {
// 将状态更新为等待延迟完成
currentState = GetDataFromNetworkState.WaitingForDelay
// 调用 suspendCoroutine 函数挂起协程,等待延迟完成
return suspendCoroutine { continuation ->
// 模拟延迟 1000 毫秒
Thread.sleep(1000)
// 延迟完成后,恢复协程执行
continuation.resume(Unit)
}
}
// 等待延迟完成的状态
is GetDataFromNetworkState.WaitingForDelay -> {
// 将状态更新为任务完成
currentState = GetDataFromNetworkState.Finished
// 返回函数的结果
return "Data from network"
}
// 任务完成的状态,结束状态机
is GetDataFromNetworkState.Finished -> {
return null
}
}
}
}
Continuation
接口来管理协程的状态。Continuation
本质上是一个回调接口,它保存了协程的上下文和状态。当协程挂起时,只需要保存当前的 Continuation
对象,而不需要像线程那样保存整个线程栈。以下是 suspendCoroutine
函数的简化示例:// 定义一个挂起函数,用于挂起协程并执行指定的代码块
suspend fun suspendCoroutine(block: (Continuation) -> Unit): T =
// 调用 suspendCoroutineUninterceptedOrReturn 函数,传入一个 lambda 表达式
suspendCoroutineUninterceptedOrReturn { c ->
// 创建一个 SafeContinuation 对象,用于保存协程的状态
val safe = SafeContinuation(c.intercepted())
// 执行传入的代码块,将 SafeContinuation 对象作为参数传递
block(safe)
// 获取协程的结果,如果协程还未完成,会抛出异常
safe.getOrThrow()
}
这里的 SafeContinuation
就是用于保存协程状态的对象,它的创建和销毁开销很小。
// 创建一个新的线程对象
Thread thread = new Thread(() -> {
// 线程执行的代码
System.out.println("Thread is running");
});
// 启动线程
thread.start();
suspend
函数就是控制协程挂起的关键。当协程遇到 suspend
函数时,会调用 Continuation
的 resumeWith
方法将控制权让出。以下是 delay
函数的简化示例:// 定义一个挂起函数,用于暂停协程指定的时间
suspend fun delay(timeMillis: Long) {
// 如果延迟时间小于等于 0,直接返回
if (timeMillis <= 0) return
// 调用 suspendCancellableCoroutine 函数挂起协程,并在指定时间后恢复
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation ->
// 调用协程上下文的 delay 调度器,在指定时间后恢复协程
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
synchronized
关键字或 Lock
接口。以下是使用 ReentrantLock
实现线程同步的示例:import java.util.concurrent.locks.ReentrantLock;
// 定义一个计数器类
class Counter {
// 计数器的值
private int count = 0;
// 创建一个 ReentrantLock 对象,用于线程同步
private final ReentrantLock lock = new ReentrantLock();
// 增加计数器的值
public void increment() {
// 获取锁
lock.lock();
try {
// 增加计数器的值
count++;
} finally {
// 释放锁
lock.unlock();
}
}
// 获取计数器的值
public int getCount() {
// 获取锁
lock.lock();
try {
// 返回计数器的值
return count;
} finally {
// 释放锁
lock.unlock();
}
}
}
withContext
函数切换线程:// 定义一个挂起函数,用于在 IO 线程中获取数据
suspend fun fetchData() = withContext(Dispatchers.IO) {
// 模拟网络请求,暂停 1000 毫秒
delay(1000)
// 返回获取到的数据
"Data from network"
}
withContext
函数内部会挂起协程,切换到指定的线程池执行任务,任务完成后再恢复协程。
Future
、Handler
等机制来处理异步操作。以下是使用 ExecutorService
和 Future
来执行异步任务的示例:import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建一个单线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交一个任务到线程池,并返回一个 Future 对象
Future future = executor.submit(() -> {
// 模拟网络请求,暂停 1000 毫秒
Thread.sleep(1000);
// 返回获取到的数据
return "Data from network";
});
try {
// 获取任务的结果,如果任务还未完成,会阻塞当前线程
String result = future.get();
// 打印任务的结果
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
// 处理异常
e.printStackTrace();
} finally {
// 关闭线程池
executor.shutdown();
}
}
}
这种方式代码逻辑比较复杂,容易出现嵌套和回调地狱的问题。
综上所述,协程在资源消耗、并发控制和代码编写方面都具有明显的优势,尤其适合处理大量的异步任务。在 Android 开发中,合理使用协程可以提高应用的性能和可维护性。