Linux C/C++ 学习日记(29):IO密集型与CPU密集型、CPU的调度与线程切换
注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
一、IO密集型与CPU密集型的定义
对比维度 | IO 密集型操作 | CPU 密集型操作 |
---|---|---|
定义 | 操作过程中主要依赖输入 / 输出(IO)交互,CPU 大部分时间处于等待 IO 完成的状态(如等待磁盘读写、网络数据传输等)。 | 操作过程中主要依赖 CPU 进行计算处理,CPU 持续处于高负载状态,IO 操作极少或耗时极短。 |
核心特征 | IO 操作频繁(如读写文件、网络通信);CPU 利用率低;任务执行时间主要消耗在 IO 等待。 | 计算任务繁重(如复杂运算、逻辑处理);CPU 利用率高(接近 100%);执行时间主要消耗在计算。 |
C/C++ 典型例子 | 1. 文件读写( 2. 网络数据收发( 3. 数据库交互(通过 API 执行查询 / 写入) | 1. 大规模数学计算(矩阵乘法、数值模拟) 2. 数据加密 / 解密(AES、RSA 算法实现) 3. 复杂算法处理(千万级数据排序、图形渲染计算) |
CPU 使用率 | 较低(大部分时间等待 IO,CPU 空闲) | 较高(持续满负载运行,充分占用 CPU 资源) |
等待时间占比 | 高(IO 等待时间占总执行时间的绝大部分) | 低(几乎无 IO 等待,总执行时间主要为计算时间) |
资源瓶颈 | 受限于 IO 设备性能(如磁盘读写速度、网络带宽) | 受限于 CPU 处理能力(如核心数量、主频、缓存大小) |
二、IO密集型场景的解决方案
在 C/C++ 中处理 IO 密集型操作(如文件读写、网络通信、数据库交互等)的核心目标是避免CPU阻塞在IO 等待的时间里、提高 CPU 利用率,并高效处理并发 IO 任务。以下是常见的解决方案及适用场景:
1. 非阻塞 IO + IO 多路复用(一般用于单线程)
单线程中、传统阻塞 IO 会导致进程 / 线程在 IO 操作时挂起(等待数据就绪),浪费 CPU 资源。非阻塞 IO 配合 IO 多路复用可解决这一问题。
(注意:要想不浪费CPU资源,你当然可以设置多个线程,当线程阻塞时,如果线程数大于内核数时,CPU会释放出去给需要的线程,但是注意线程的数量是有限制的(在Linux中每个线程占8M空间),一般应用于fd数量很少的时候,在高并发场景中我们不会采用一IO一线程的形式!!!)
1.1 非阻塞 IO
通过fcntl
或ioctl
将文件描述符(fd)设置为非阻塞模式,此时调用read
/write
等 IO 函数时:
- 若数据未就绪(或缓冲区满),函数会立即返回错误(
EAGAIN
/EWOULDBLOCK
),而非阻塞等待; - 进程可继续执行其他任务,避免闲置。
1.2 IO 多路复用(核心)
用于同时监控多个 IO 流的就绪状态(可读 / 可写 / 异常),避免对每个 IO 流单独轮询,大幅提升效率。常见实现:
机制 | 特点 | 适用场景 |
---|---|---|
select | 支持跨平台,但 fd 数量有限(默认 1024),每次调用需复制 fd 集合,效率低 | 简单场景、低并发 |
poll | 突破 fd 数量限制,但仍需轮询所有 fd,高并发时效率下降 | 中等并发、跨平台需求 |
epoll | Linux 专属,事件驱动(只关注活跃 fd),无 fd 数量限制,效率极高 | 高并发场景(如服务器) |
kqueue | BSD/macOS 专属,类似 epoll,支持更多事件类型(如文件修改) | BSD/macOS 系统的高并发场景 |
(我之前写了一篇IO多路复用的代码实现的博文,可以去找一找)
1.3 总结:
维度 | 具体内容 |
---|---|
核心原理 | 1. 非阻塞 IO:通过 2. IO 多路复用:通过内核机制(select/poll/epoll/kqueue 等)同时监控多个 fd 的就绪状态(可读 / 可写 / 异常),仅处理活跃 IO 流。 |
解决的问题 | 传统阻塞 IO 中,进程 / 线程会因等待 IO 而挂起,导致 CPU 资源浪费;单线程无法高效处理大量并发 IO 流的问题。 |
优点 | - 避免进程 / 线程阻塞等待 IO,CPU 利用率高; - 事件驱动模式,仅处理就绪的 IO 流,减少无效操作,效率高; - epoll/kqueue 支持无上限监控 fd,适配高并发场景。 |
缺点 | - select:默认 fd 上限 1024(调整意义有限),每次调用需复制 fd 集合,高并发下效率低; - poll:无 fd 数量限制,但需轮询所有 fd,高并发时效率随 fd 数量增加而下降;- 编程复杂,需手动处理非阻塞 IO 逻辑和事件回调,易出现 “回调地狱”。 |
应用场景 | - 高并发网络服务(如 Web 服务器、网关、即时通讯服务器); - 需要同时处理大量 IO 流(如 thousands 级以上连接)的场景。 |
注意事项 | - 高并发场景优先选择 epoll(Linux)或 kqueue(BSD/macOS),避免使用 select/poll; - 需熟练处理非阻塞 IO 的返回值(如区分 - 事件回调逻辑需简洁,避免阻塞事件循环。 |
2. 多线程 / 线程池
IO 密集型任务中,CPU 大部分时间处于空闲(等待 IO),因此可通过多线程并行处理多个 IO 任务,利用 CPU 多核资源。
注意:当线程阻塞时,CPU会释放,但是线程的数量是有限制的
a. 在Linux中每个线程占8M空间
b. 过多线程会导致上下文切换开销激增。
所以线程一般应用于fd数量很少的时候,在高并发场景中我们不会采用一IO一线程的形式!!!
2.1 核心思路
- 每个线程处理一个独立的 IO 流(如一个网络连接、一个文件);
- 当线程阻塞在 IO 时,其他线程可继续运行,避免 CPU 闲置。
2.2 线程池优化
频繁创建 / 销毁线程会带来开销,线程池通过预先创建固定数量的线程,循环处理任务队列中的 IO 任务,减少开销:
2.3 总结:
维度 | 具体内容 |
---|---|
核心原理 | 1. 多线程:每个线程独立处理一个 IO 流(如一个网络连接、一个文件),当线程因 IO 阻塞时,CPU 自动调度其他就绪线程运行; 2. 线程池:预先创建固定数量的线程,循环从任务队列中获取 IO 任务并处理,减少线程创建 / 销毁的开销。 |
解决的问题 | 单线程串行处理多个 IO 流时效率低下的问题,通过多核 CPU 并行处理 IO 任务,提高整体吞吐量。 |
优点 | - 利用多核 CPU 并行处理,提升 IO 任务吞吐量; - 基于同步阻塞 IO 模型,编程逻辑简单,开发和维护成本低; - 线程池避免频繁创建 / 销毁线程的开销,适合任务频繁的场景。 |
缺点 | - 线程栈内存占用大(Linux 默认 8MB,可调整但仍有下限),IO 流过多时易导致内存压力; - 线程上下文切换为内核级操作,开销高(约几微秒),线程数量过多会抵消并行收益; - 线程间共享数据时易出现锁竞争,导致核心阻塞。 |
应用场景 | - IO 流数量较少的场景(如几十到几百个连接 / 文件); - 任务逻辑简单、IO 阻塞时间较长的场景(如少量文件批量读写、低并发数据库交互)。 |
注意事项 | - 线程数量建议控制在 “CPU 核心数 ×2” 以内,避免过多线程导致切换开销激增; - 可通过 - 减少线程间共享数据,优先使用无锁结构或线程本地存储,避免锁竞争。 |
3. 协程(轻量并发)
(关于协程的实现我会在后续博文详细介绍)
协程是用户态的 “轻量级线程”,在 IO 操作时可主动挂起,让出 CPU 给其他协程,避免线程上下文切换的开销,适合处理海量并发 IO(如十万级网络连接)。
3.1 C++ 中的协程
- C++20 引入
std::coroutine
,但需配合编译器支持(如 GCC 10+、Clang 15+); - 第三方库:Boost.Coroutine、libco(腾讯)等,封装了协程切换逻辑。
3.2 优势
- 协程切换成本远低于线程(无需内核参与);
- 单线程可运行上万协程,高效处理大量 IO 任务。
- 用同步的代码风格写异步逻辑
(注意:协程的切换通常搭配epoll实现)
3.3 总结:
维度 | 具体内容 |
---|---|
核心原理 | 协程是用户态的轻量级并发单元,拥有独立的栈空间但由用户态调度; 当执行 IO 操作时,协程主动挂起(用户态切换,无需内核参与),让出 CPU 给其他协程; 依赖底层 IO 多路复用(如 epoll)监控 IO 就绪状态,当 IO 就绪时唤醒对应的协程。 |
解决的问题 | 多线程在高并发场景下内存占用大(线程栈)、上下文切换开销高的问题,支持海量 IO 流(十万级以上)的高效并发处理。 |
优点 | - 协程切换为用户态操作,开销极低(约几十纳秒,仅为线程切换的 1/100~1/1000); - 单线程可承载上万甚至几十万协程,支持海量并发 IO; - 支持用同步代码风格编写异步逻辑,避免 “回调地狱”,可读性和可维护性高。 |
缺点 | - 需依赖底层 IO 多路复用(如 epoll)实现 IO 就绪通知,否则协程挂起后无法被唤醒; - 协作式调度,若某协程执行 CPU 密集型操作(长时间不挂起),会阻塞同一线程内的其他协程; - 依赖第三方库(如 Boost.Coroutine、libco)或 C++20 标准协程(编译器支持有限,使用门槛高)。 |
应用场景 | - 十万级以上海量并发 IO 场景(如高并发客户端、分布式爬虫、微服务网关); - 每个任务包含多次 IO 等待的场景(如多次数据库查询、多步网络请求)。 |
注意事项 | - 必须与 IO 多路复用(如 epoll)配合使用,否则无法发挥高效并发能力; - 避免在协程中执行长时间 CPU 密集型操作,需主动挂起让出 CPU; - 根据开发环境选择合适的协程实现(优先成熟第三方库,C++20 标准协程需确认编译器支持)。 |
三、CPU密集型的解决方案
在 C/C++ 中,CPU 密集型操作(如数值计算、图像处理、加密解密、复杂算法推理等)的核心瓶颈是CPU 计算能力,解决方案的核心目标是最大化利用 CPU 资源(如多核并行、指令级优化、减少计算冗余等)。以下是具体方案及适用场景:
1.多线程并行(利用多核 CPU)
现代 CPU 普遍为多核架构,单线程只能利用一个核心,多线程可将任务拆分到多个核心并行执行,直接提升计算吞吐量。
1.1 核心思路
- 任务拆分:将 CPU 密集型任务按逻辑拆分为独立子任务(如分块处理数组、分片计算矩阵),每个子任务由一个线程执行;
- 线程数控制:线程数通常设置为CPU 核心数(或核心数 ±1),避免过多线程导致上下文切换开销抵消并行收益(CPU 密集型任务中,线程切换成本极高:当线程数大于内核数时,如果每个线程都需要CPU,那么CPU会进行轮询。轮询的时间成本高于CPU实际计算的时间)。
2.2 实现方式
原生线程(pthread/C++11 thread):手动创建线程并分配任务,适合简单场景。
线程池:避免频繁创建 / 销毁线程的开销(尤其任务短而多的场景),通过预先创建固定数量的线程循环处理任务队列。适合任务粒度小、动态生成的场景(如批量图像处理)。
2.3 注意事项
- 避免锁竞争:CPU 密集型任务中,线程间若频繁竞争锁(如共享数据写入),会导致大量核心阻塞,严重降低性能。建议:
- 尽量使用无锁数据结构(如
std::atomic
)或线程本地存储(TLS); - 拆分任务时让数据无重叠(每个线程处理独立数据块),减少共享。
- 尽量使用无锁数据结构(如
- 负载均衡:确保子任务计算量均匀,避免 “某线程早结束,其他线程仍在忙碌” 的情况(可通过动态任务分配优化,如 TBB 的
parallel_for
)。
2.4 总结:
维度 | 具体内容 |
---|---|
核心原理 | 基于多核 CPU 架构,将 CPU 密集型任务按逻辑拆分为独立子任务(如数据分块、计算分片),通过多线程将子任务分配到不同 CPU 核心并行执行; 线程由操作系统内核调度,当线程数与核心数匹配时,可最大化利用多核计算能力。 |
解决的问题 | 单线程只能利用单个 CPU 核心,无法发挥多核架构的计算潜力,导致 CPU 密集型任务(如数值计算、图像处理)执行效率低下的问题。 |
优点 | - 高效利用多核资源:通过并行计算直接提升吞吐量,理论上 N 核 CPU 可实现近 N 倍加速(忽略任务拆分开销); - 线程通信成本低:线程共享进程地址空间,可直接访问共享内存(需同步机制),数据交互效率高于进程间通信; - 线程池优化:预先创建线程减少频繁创建 / 销毁的开销,适合短任务、动态生成任务的场景。 |
缺点 | - 上下文切换开销高:当线程数超过 CPU 核心数时,内核需频繁切换线程(保存 / 恢复寄存器、栈等状态),每次切换约几微秒,会抵消并行收益; - 同步成本风险:线程共享数据时需锁或原子操作,频繁锁竞争会导致核心阻塞(“串行化”),严重降低性能; - 资源限制:每个线程默认栈空间较大(Linux 约 8MB),过多线程会占用大量内存。 |
应用场景 | - 可拆分为独立子任务的 CPU 密集型操作:如大数组运算、矩阵乘法、图像分块处理、批量加密解密; - 任务粒度适中且可动态生成的场景:如实时视频帧处理、批量数据清洗与转换。 |
注意事项 | - 线程数控制:通常设为 “CPU 核心数” 或 “核心数 ±1”,避免线程数超过核心数导致切换开销激增;- 减少共享与锁竞争:优先采用 “数据无重叠拆分”(每个线程处理独立数据块),必要时用 - 负载均衡:确保子任务计算量均匀(如通过动态任务分配,如 TBB 的 |
2. 多进程并行(隔离性与多核利用)
多进程与多线程的核心区别是内存隔离(进程间不共享地址空间),适合以下场景:
- 任务稳定性要求高(单个进程崩溃不影响其他进程);
- 利用操作系统的 CPU 调度(避免线程库调度的局限性)。
2.1 实现方式
- 通过
fork
(Linux)或CreateProcess
(Windows)创建子进程,通过进程间通信(IPC) 传递数据(如管道、共享内存、消息队列)。 - 共享内存(如
shmget
/mmap
)是 CPU 密集型任务的优选 IPC 方式(避免数据拷贝开销)。
2.2 优劣
- 优势:隔离性好,适合不稳定任务;可利用操作系统调度均衡负载。
- 劣势:IPC 开销高于线程间共享内存;进程创建 / 销毁成本高于线程。
2.3 总结:
维度 | 具体内容 |
---|---|
核心原理 | 通过创建多个独立进程(如 Linux 的fork 、Windows 的CreateProcess ),将 CPU 密集型任务拆分到不同进程,利用多核 CPU 并行执行;进程间内存隔离(不共享地址空间),需通过进程间通信(IPC)传递数据或同步状态。 |
解决的问题 | - 单线程进程无法利用多核 CPU,导致 CPU 密集型任务效率低下; - 多线程方案中 “线程崩溃可能导致整个进程崩溃” 的稳定性风险,以及线程库调度在某些场景下不如系统级进程调度灵活的局限性 |
优点 | - 隔离性强:进程拥有独立地址空间,单个进程崩溃(如内存越界、断言失败)不会影响其他进程,适合稳定性要求高的任务(如异构计算节点、第三方不可靠模块); - 调度更灵活:可利用操作系统原生调度机制(避免线程库调度的局限性),在复杂负载下可能获得更均衡的核心利用; - 资源隔离:进程间内存、文件描述符等资源独立,可避免线程间的资源竞争(如堆内存碎片影响)。 |
缺点 | - IPC 开销高:进程间数据交互需通过管道、消息队列、共享内存等方式,其中共享内存虽高效但需手动管理同步,其他方式(如管道)存在数据拷贝开销,效率低于线程间直接共享; - 管理成本高:进程创建 / 销毁比线程更耗时(涉及地址空间初始化、页表创建等),不适合短任务频繁生成的场景; - 内存占用高:每个进程需独立加载代码、数据,重复占用内存(如相同库的代码段在多进程中可能被共享,但堆 / 栈独立)。 |
应用场景 | - 稳定性优先的 CPU 密集型任务:如分布式计算节点(单个节点崩溃不影响集群)、集成第三方不稳定算法(如实验性模型推理); - 需资源隔离的场景:如多用户计算任务(避免用户间资源干扰)、对内存泄漏敏感的长期运行任务; - 利用系统级调度优势的场景:如跨 NUMA 节点的大规模计算(进程更易绑定到特定 NUMA 节点)。 |
注意事项 | - 优先选择高效 IPC:CPU 密集型任务中,推荐用共享内存(如 - 进程数控制:与线程类似,进程数建议不超过 CPU 核心数(避免调度开销),或根据 NUMA 节点数合理分配; - 避免重复初始化:通过共享库或预加载数据减少多进程的重复初始化开销(如提前加载模型权重到共享内存)。 |
四、线程的状态
状态名称 | 定义(核心特征) | 触发条件(进入该状态的原因) | 转换方向(可进入的其他状态) |
---|---|---|---|
新建(New) | 线程已被创建(如通过pthread_create 或std::thread ),但尚未启动(未调用start 或未执行)。 | 调用线程创建函数(如pthread_create )后,线程对象初始化完成但未开始执行。 | 调用启动接口(如std::thread::joinable 状态下开始执行)后,进入就绪状态。 |
就绪(Ready) | 线程已启动,具备执行条件(CPU 资源除外),等待操作系统调度分配 CPU 时间片。 | 线程新建后启动;或阻塞状态结束(如 IO 完成、锁释放); 或被抢占后重新进入就绪队列。 | 被操作系统调度获得 CPU 时间片后,进入运行状态。 |
运行(Running) | 线程正在 CPU 核心上执行指令,占用 CPU 资源。 | 从就绪状态被调度器选中,获得 CPU 时间片。 | 时间片耗尽或被高优先级线程抢占,回到就绪状态;或因等待资源(如锁、IO),进入阻塞状态;或执行完毕,进入终止状态。 |
阻塞(Blocked) | 线程暂时停止执行,放弃 CPU 资源,等待特定条件满足(如 IO 完成、锁释放、信号通知)。 | 1. 等待互斥锁(如 2. 执行 IO 操作(如 3. 调用阻塞式同步接口(如 | 等待的条件满足(如锁释放、IO 完成)后,进入就绪状态。 |
等待(Waiting) | 阻塞状态的细分,线程无超时等待某个事件(需被其他线程显式唤醒)。(部分模型中独立为状态) | 调用无超时等待接口(如pthread_cond_wait 、std::condition_variable::wait )。 | 被其他线程唤醒(如pthread_cond_signal )后,进入就绪状态。 |
超时等待(Timed Waiting) | 阻塞状态的细分,线程有超时等待某个事件(超时后自动唤醒)。(部分模型中独立为状态) | 调用带超时的等待接口(如pthread_cond_timedwait 、sleep 、std::this_thread::sleep_for )。 | 超时时间到达或被提前唤醒后,进入就绪状态。 |
终止(Terminated) | 线程执行完毕(如函数返回)或被强制终止(如pthread_cancel ),生命周期结束。 | 1. 线程函数正常返回; 2. 调用终止接口(如 3. 被其他线程取消(如 | 无后续状态转换,线程对象可能保留(如std::thread 的joinable 状态需join 或detach 释放资源)。 |
说明:
- 状态划分的灵活性:部分系统(如 Linux)将 “就绪” 和 “运行” 合并为 “可运行(R)” 状态(内核视角下,两者均为就绪队列中的任务,仅运行状态是当前占用 CPU 的任务)。
- 阻塞状态的细分:“等待”“超时等待” 是阻塞状态的特殊场景,核心都是线程放弃 CPU 等待资源,部分模型中会将其独立为状态以更清晰描述等待类型。
- 核心逻辑:线程状态转换的核心是”是否需要CPU“ “是否占用 CPU” 和 “是否等待资源”,从新建到终止的生命周期围绕这两点展开。
总的来说:线程的状态影响着操作系统是否分配CPU给它!!!
五、CPU调度情况
1.线程数不同时
维度 | 线程数 ≤ CPU 内核数 | 线程数 > CPU 内核数 |
---|---|---|
CPU 利用率 | 高,每个线程可在独立内核上并行执行,无闲置核心 | 理论上仍为 100%,但实际因上下文切换开销,有效计算时间被占用,实际有效利用率下降 |
上下文切换开销 | 低,线程间切换主要为CPU的主动让出(如阻塞、线程结束等等) | 高,内核需频繁切换线程(保存 / 恢复线程状态),切换开销占比显著增加 |
任务吞吐量 | 随线程数增加线性提升(接近内核数时达到峰值) | 吞吐量先增后减,线程数超过内核数后,因切换开销抵消并行收益,吞吐量开始下降 |
资源消耗(内存等) | 低,每个线程栈空间(如 Linux 默认 8MB)总消耗可控 | 高,过多线程的栈空间、内核调度数据结构会占用大量内存,可能引发内存压力 |
适用场景 | CPU 密集型任务(如数值计算、图像处理),需最大化核心利用率 | IO 密集型任务(如网络通信、文件读写),线程因 IO 阻塞时可让出 CPU 给其他线程 |
性能表现 | 响应时间稳定,无额外调度延迟 | 响应时间波动大,调度延迟增加(线程需等待 CPU 时间片) |
2. 抢占式调度
项目 | 具体内容 |
---|---|
核心结论 | 当线程数 > CPU 内核数时,操作系统对线程的调度属于抢占式调度。 |
原因分析 | 现代操作系统(如 Linux、Windows)的线程调度机制默认是抢占式的: 当线程数超过 CPU 内核数时,多个线程需共享有限的 CPU 核心,操作系统会为每个线程分配时间片(一段可执行的时间);时间片耗尽后,操作系统会主动中断当前线程(抢占其 CPU 使用权),并切换到下一个线程执行,以保证所有线程公平获得 CPU 资源,避免某线程长期占用 CPU 导致其他线程 “饥饿”。 |
具体表现 | - 每个线程会被分配时间片(如 Linux 默认时间片约为几毫秒),时间片内线程可独占一个 CPU 核心执行; - 时间片耗尽后,操作系统会强制切换到下一个线程,即使当前线程的计算任务未完成。 |
补充说明 | - 抢占式调度是内核级的,由操作系统内核主动控制,与线程是否 “需要 CPU” 无关,只要线程处于就绪状态,就可能被调度器抢占或分配 CPU 时间; - 若线程因 IO 操作阻塞(如网络读写),会主动让出 CPU(属于 “协作式让出”),但整体调度机制仍为抢占式(其他就绪线程会被抢占式调度执行)。 |
3. 非抢占式调度
项目 | 具体内容 |
---|---|
核心结论 | 当线程数 ≤ CPU 内核数时,操作系统对线程的调度以 “非抢占为主,必要时抢占”(多数场景下线程可在分配的核心上持续执行,减少强制切换)。 |
原因分析 | 现代操作系统虽默认采用抢占式调度机制,但此时线程数不超过核心数,每个线程可分配到独立的 CPU 核心,无需共享核心资源: - 若线程均为 CPU 密集型且优先级相同,操作系统通常不会主动抢占,允许线程在核心上持续执行(减少切换开销); - 仅在特殊场景(如高优先级线程就绪、当前线程触发中断或阻塞)时,才会触发抢占式切换。 |
具体表现 | - 线程可在分配的 CPU 核心上 “长时运行”,时间片限制较宽松(甚至不严格耗尽时间片),上下文切换极少; - 若某线程因 IO 操作阻塞或主动让出 CPU(如 - 高优先级线程就绪时,仍会抢占低优先级线程的核心(保证实时性)。 |
补充说明 | - 此场景下 CPU 利用率接近 100%(无闲置核心),且上下文切换开销极低,适合 CPU 密集型任务(如数值计算、图像处理); - “非抢占为主” 不代表完全无抢占,操作系统仍会通过抢占机制保证高优先级任务的响应性(如实时线程、中断处理); - 若线程数小于核心数,空闲核心会处于 “idle” 状态,操作系统可能将线程迁移到空闲核心以平衡负载(非抢占,属于主动调度优化)。 |
注意:本质上还是采取抢占式(还是有时间片的设置),只是每个线程可以得到单独的CPU核心,时间片很长罢了!!!表现为操作系统不频繁调度CPU资源。
六、CPU调度与线程切换的关系
线程切换是操作系统进行 CPU 调度时的核心操作之一,二者紧密关联但不完全等同。
具体关系拆解:
CPU 调度(Scheduling) 是一个决策过程:操作系统的调度器根据策略(如优先级、时间片、公平性等),从就绪队列中选择下一个要在 CPU 上执行的线程。例如:当线程时间片耗尽、被高优先级线程抢占、或主动阻塞时,调度器需要决定 “接下来让哪个线程运行”。
线程切换(Context Switch) 是执行过程:当调度器选定下一个线程后,操作系统会实际执行 “从当前线程切换到目标线程” 的动作 —— 保存当前线程的状态(寄存器、程序计数器、栈指针等),加载目标线程的状态,让其在 CPU 上继续执行。
总结:
线程切换是 CPU 调度过程中 “执行决策” 的关键步骤,没有线程切换,调度器的决策无法落地;而 CPU 调度则包含了 “选择线程” 和 “执行切换” 的完整逻辑。因此,线程切换可以理解为操作系统 CPU 调度机制中最核心的操作环节。
七、要点(核心总结)
1. IO密集型、CPU密集型
IO密集型表现为频繁IO等待(非本线程处理数据):在高性能网络中、我们需要做到不阻塞,避免浪费CPU性能(可以采用非阻塞IO加epoll、多线程、协程)
CPU密集型表现为频繁数据处理(本线程处理数据):我们需要做到充分利用CPU的核数、因为一线程只能对应一核,我们可以采用多线程(注意线程数小于等于核数,避免线程切换开销)、多进程的处理方案。
2. CPU的调度情况
线程的状态和个数与系统对CPU资源的调度息息相关:
我们必须理解线程数的设置如何影响CPU调度的频率、同时理解操作系统是如何根据线程状态来进行CPU资源的分配的!!!
3. 以上的知识有助于后面理解协程
大家一定要理解上面这些,因为内核的操作开销都是比较大的(我们必须想办法降低开销)。
线程的数量的限制都是为了性能:线程数量大了、占用内存大、同时切换开销大(延迟高、不适合高并发IO场景)、切换规则也不容易更改。
了解了这些限制你才会明白为什么需要在用户态又搞个协程。
协程又称为轻量级线程,它也有轻量栈、状态、调度器(类似一个管理CPU分配的内核)。它相比于线程(内核管理,开销大、不灵活)、更加灵活轻量、CPU的分配完全由用户自定义。
(我会在后续博文详细介绍协程给大家,包括协程的优点、理念、以及协程库的实现和妙用!!!!)