Android学习总结之协程对比优缺点(协程一)

        进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位;

        线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位;

        而协程是一种比线程更加轻量级的并发编程方式,它可以在一个线程中实现多个任务的并发执行。

协程比线程使用资源更少的原因
  • 栈空间小:线程的栈空间一般为几 MB,而协程的栈空间通常只有几 KB,大大减少了内存的占用。
  • 创建和销毁开销低:线程的创建和销毁需要操作系统内核的参与,涉及到系统调用,开销较大;而协程的创建和销毁在用户态完成,开销较小。
  • 上下文切换开销小:线程的上下文切换需要保存和恢复寄存器、栈指针等信息,并且涉及到用户态和内核态的转换;而协程的上下文切换只需要保存和恢复少量的寄存器信息,开销较小。

进程、线程、协程对比表

维度 进程(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 时间片,影响同进程内其他线程。 协程阻塞不阻塞线程,可释放线程执行其他协程。

核心差异总结

  1. 调度层级

    • 进程和线程由 操作系统内核 调度,属于 内核态并发
    • 协程由 协程库 调度,属于 用户态并发,依赖于线程但更轻量。
  2. 资源开销

    • 进程:资源隔离性最强,但创建和切换开销最大;
    • 线程:共享进程资源,开销中等;
    • 协程:几乎无额外资源开销,可在单线程内运行数万个协程。
  3. 控制方式

    • 进程 / 线程:由操作系统强制抢占,不可预测;
    • 协程:通过 suspend 主动挂起,协作式恢复,适合细粒度异步控制。
  4. 适用场景

    • 进程:适合完全隔离的独立任务;
    • 线程:适合 CPU 密集型或需要系统级并发的任务;
    • 协程:适合 I/O 密集型、高并发轻量任务(如网络请求、UI 异步更新)。

关于 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
            }
        }
    }
}

协程和多线程 / 线程池的区别

1. 资源消耗
  • 协程
    协程是轻量级的,一个线程可以容纳多个协程。从源码角度看,Kotlin 协程使用 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 就是用于保存协程状态的对象,它的创建和销毁开销很小。

  • 多线程 / 线程池
    线程在创建时,操作系统会为其分配一定的栈空间,通常是几兆字节。线程的创建和销毁涉及到操作系统的内核调用,开销较大。线程池虽然可以复用线程,但每个线程仍然需要占用固定的栈空间,当线程数量较多时,会占用大量的系统内存。以下是 Java 中创建线程的示例:
// 创建一个新的线程对象
Thread thread = new Thread(() -> {
    // 线程执行的代码
    System.out.println("Thread is running"); 
});
// 启动线程
thread.start(); 
2. 并发控制
  • 协程
    协程是协作式的并发,由开发者控制协程的挂起和恢复。在 Kotlin 协程中,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) 
    }
}
  • 多线程 / 线程池
    多线程是抢占式的并发,由操作系统调度。线程之间竞争 CPU 资源,为了保证线程安全,需要使用同步机制,如 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(); 
        }
    }
}
3. 代码编写
  • 协程
    协程可以以同步的方式编写异步代码,避免了回调地狱。例如,使用 withContext 函数切换线程:
// 定义一个挂起函数,用于在 IO 线程中获取数据
suspend fun fetchData() = withContext(Dispatchers.IO) {
    // 模拟网络请求,暂停 1000 毫秒
    delay(1000) 
    // 返回获取到的数据
    "Data from network" 
}

withContext 函数内部会挂起协程,切换到指定的线程池执行任务,任务完成后再恢复协程。

  • 多线程 / 线程池
    多线程编程需要使用回调、FutureHandler 等机制来处理异步操作。以下是使用 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 开发中,合理使用协程可以提高应用的性能和可维护性。

你可能感兴趣的:(Android学习知识总结,android,学习,java)