• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2024-10-31 12:28 Aet 隐藏边栏 |   抢沙发  10 
文章评分 2 次,平均分 5.0

超线程技术

概述

  1. 现代CPU通常确实在物理核心的基础上引入逻辑核心,例如通过超线程(Hyper-Threading)技术,一个物理核心能够处理两个线程
    1. 使得操作系统和应用程序认为该CPU拥有的核心数量翻倍
  2. 不过,CPU的逻辑核心和线程的概念在软件开发中存在一些区别
    1. 逻辑核心更偏向于物理硬件资源的虚拟化
    2. 而线程在编程中是对操作系统和CPU资源的一种使用方式,意味着在软件中能执行的独立指令序列

逻辑核心和线程的理解

  1. 逻辑核心确实属于 CPU硬件的处理能力 范畴,通过技术(如超线程)让物理核心同时运行多个任务
  2. 线程则是操作系统用来 组织和管理程序并发 的基本单位
    1. 操作系统会将不同的线程分配到不同的逻辑核心上执行

线程

概述

  1. 线程是程序的基本执行单元,是调度和分配 CPU 时间的基本实体
  2. 一个程序可以包含一个或多个线程,每个线程独立执行

状态

  1. 新建状态(New):线程被创建但未开始运行
  2. 就绪状态(Ready):线程准备好执行,等待操作系统分配 CPU
  3. 运行状态(Running):线程被 CPU 调度器分配时间片,正在执行
  4. 阻塞状态(Blocked/Waiting):线程因等待资源或事件而暂停执行,不占用 CPU
  5. 终止状态(Terminated):线程完成了执行或因异常终止

线程如何向 CPU 提出执行请求

  1. 就绪队列
    1. 当线程创建并进入就绪状态时,它会被放入操作系统的就绪队列中
    2. 就绪队列中的线程表示它们已经准备好执行,等待 CPU 分配时间片
  2. 调度请求
    1. 线程向 CPU 提出执行请求的本质是被操作系统的调度器从就绪队列中选择并分配 CPU 时间片
    2. 线程本身不会主动请求 CPU,而是由调度器根据调度算法(如轮询调度、优先级调度等)来决定哪个线程可以执行
  3. 线程的状态转换
    1. 就绪到运行:当调度器选择某个线程执行时,线程从就绪状态切换到运行状态
    2. 运行到阻塞:线程因等待 I/O 操作、锁定资源或等待事件进入阻塞状态
    3. 阻塞到就绪:当等待条件满足(如 I/O 完成、资源释放),线程返回就绪队列等待重新调度

CPU 如何响应线程的执行请求

  1. CPU 调度器
    1. CPU 调度器是操作系统内核的一部分,负责在就绪队列中选择线程,将 CPU 时间片分配给它们
  2. 时间片
    1. 每个线程在被调度到 CPU 上时会分配一个时间片
    2. 时间片结束后,线程会被挂起,切换到另一个线程
    3. 通过时间片轮转,CPU 可以在多个线程之间快速切换,形成线程“并发”执行的效果
  3. 中断和抢占
    1. CPU 调度依赖于时钟中断,时钟中断是调度器抢占正在运行的线程并选择下一个线程执行的时机
    2. 抢占机制确保高优先级的线程可以优先执行,即使它们在低优先级线程的时间片内出现

调度器的工作机制

  1. 上下文切换(Context Switch
  2. 当调度器决定让某个线程运行时,会执行上下文切换
  3. 上下文切换包括保存当前运行线程的状态(寄存器、程序计数器等),然后加载即将运行的线程的状态

理解

  1. 程序 A 的线程可以被CPU的任何一个逻辑核心执行,具体由操作系统的调度器决定
  2. 而任何一个逻辑核心在执行某个线程时,都可以通过线程本地存储(Thread Local Storage, TLS)和线程的上下文信息来加载并恢复该线程的最后一次执行状态,从而继续执行之前未完成的任务
  3. 总体来讲,每个逻辑核心虽然任何时刻只能执行一个线程,但由于时间片分配非常短(通常在几毫秒级别),逻辑核心会很快得频繁切换执行不同的线程,使得我们感知不到这个交替过程

CPU 执行线程的详细过程

  1. 线程上下文保存与恢复
    1. 当一个线程的时间片结束或被调度器挂起时,操作系统会保存该线程的上下文信息
      包括:
      寄存器状态(如通用寄存器、程序计数器、栈指针等)
      线程本地存储(TLS):用于存储线程特定的数据
      线程栈:保存函数调用、局部变量、返回地址等
    2. 当线程被重新调度时,这些保存的上下文会被加载到 CPU 的寄存器中,使得线程可以从上次暂停的位置继续执行
  2. 逻辑核心执行线程的机制
    1. 当调度器决定让一个线程在某个逻辑核心上运行时,CPU 会通过上下文切换机制恢复线程的执行状态
    2. CPU 将寄存器恢复到该线程最后一次保存的状态,包括程序计数器(PC)的位置,这个计数器指向下一条待执行的指令地址
  3. 指令的获取和执行
    1. 取指令,如下:
    2. 程序计数器中的值通过 地址总线 被发送到系统的内存控制器
      (通常是 RAM 中的某个物理地址,也就是程序的代码段)
    3. 内存控制器接收地址请求后,通过 数据总线 返回该地址处存储的指令(通常是机器码形式的指令)
  4. 指令的解码与执行
    1. 解码(Decode
      CPU 解析指令,确定要执行的操作类型(例如算术运算、逻辑运算、内存访问等)
    2. 执行(Execute
      CPU 执行指令,完成指令中指定的操作(如寄存器之间的数据传输、内存读写、算术逻辑运算等)
  5. 指令结果的处理
    1. 执行结果可能会更新 CPU 的寄存器、改变内存中的数据,或者修改程序计数器的值以指向下一条指令
  6. 循环重复执行
    1. 这一过程会不断重复:取指令、解码、执行、更新,直到时间片结束或线程被中断
  7. 总结:
    1. 当调度器决定让一个线程在某个逻辑核心上运行时,CPU 会通过上下文切换机制恢复线程的执行状态
    2. 然后,程序计数器中的值通过 地址总线 被发送到系统的内存控制器
    3. 内存控制器接收地址请求后,通过 数据总线 返回该地址处存储的指令
    4. 在指令通过数据总线传输到 CPU 之前,内存控制器会发出一个“数据就绪”信号,告知 CPU 数据已经准备好
    5. CPU 接收到“数据就绪”信号之前,可能会进入短暂的等待状态,确保不会在数据未传输完成时开始操作
    6. 一旦数据总线上的指令准备好,CPU 会锁存(Latch)这些数据到内部指令寄存器(Instruction Register, IR)中
    7. CPU 的时钟信号与控制总线的“数据就绪”信号同步,以确保数据被正确接收
    8. CPU 会检查控制信号的状态,以确定数据传输是否成功,例如通过错误检测信号(如奇偶校验、ECC 检查)来确保数据完整性
    9. CPU 将指令从指令寄存器中读取,并通过解码单元解析该指令
    10. 根据解码结果,执行相应的操作(如算术、逻辑、内存访问等)

协程

概述

  1. 与线程不同,协程是一种 更加轻量级的并发执行单位
  2. 协程通常不依赖CPU的物理或逻辑核心,而是依赖于 用户代码控制的上下文切换,通过在特定位置暂停和恢复执行来实现

原理

  1. 协作式切换
    1. 协程的切换由代码中显式的指令控制(如 yieldawait),不会被抢占
    2. 协程可以在一段逻辑执行完成后,交还控制权,而线程则通常依赖操作系统的调度器来决定什么时候被暂停或恢复
  2. 不占用系统线程
    1. 协程在同一线程中切换执行,因此可以在不增加线程负担的前提下实现并发
    2. 而线程是由操作系统调度的实体,会占用CPU资源
  3. 更轻量的资源开销
    1. 由于不依赖系统调用,协程的创建和切换速度远高于线程,且由于不会频繁切换CPU核心,也更节省内存
  4. 适用于I/O密集型任务
    1. 协程常用于高并发I/O密集的场景(例如网络请求、文件读取),能够通过在等待时切换协程来提高效率,但它们通常不适合CPU密集型任务

理解

  1. 线程 是由操作系统管理的并发执行单元,属于 系统级别 的资源,由操作系统内核调度和分配,直接与CPU的物理或逻辑核心绑定
  2. 协程 则是在 程序员级别 管理的,属于用户空间的轻量级并发机制
    1. 协程的调度是由程序员通过代码显式控制的,不依赖操作系统的调度器
    2. 可以把协程理解为一种 对线程的封装或使用,因为协程一般会在某个线程上运行,并通过用户代码在这个线程内部切换执行的任务

总结1

  1. 协程共享同一线程
    1. 多个协程运行在同一个线程内,因此不需要额外的线程创建、销毁开销,也不会涉及线程切换的上下文切换
  2. 协作式调度
    1. 协程的执行顺序是由代码显式控制的(例如通过 awaityield 等关键字),不会被操作系统强制打断
    2. 协程可以在一个任务暂时不需要继续时(比如等待 I/O 操作完成)主动让出控制权,从而让其他协程在同一个线程上执行
  3. 适合高并发I/O任务
    1. 由于切换开销小,协程特别适合高并发的 I/O 密集型任务(例如网络请求、文件读写等)
    2. 当一个协程等待时,另一个协程可以马上执行,从而提高资源利用率
  4. 不适合CPU密集任务
    1. 协程一般在单线程环境中执行,CPU密集型任务会占满该线程的计算资源,阻塞其他协程的执行

总结1示例

  1. 在一个简单的应用中,假设有一个线程上运行着三个协程
    1. 协程A:负责下载文件
    2. 协程B:处理用户输入
    3. 协程C:写入日志文件
  2. 在下载、读取输入或写入时,任何协程如果遇到等待,就可以主动让出控制权,允许其他协程运行

总结2

  1. 协程的切换完全发生在 线程内部,所以对于CPU来说,这个线程看起来与普通线程并无不同
    1. CPU的角度来看,执行的指令流就是线程级别的
    2. CPU只需要根据线程的上下文继续执行指令,而至于这些指令是属于协程A的任务还是协程B的任务,CPU并不关心,操作系统也不干预
  2. 对于协程来说,协程的切换由程序中的代码逻辑(例如 yieldawait)控制
    1. 协程可以在特定的等待点让出控制权给同线程的其他协程
    2. 这种切换是用户级的,也就是由程序自行管理,不涉及CPU和操作系统的调度

总结2示例

  1. 在一个简单的应用中,假设有一个线程上运行着三个协程
    1. 协程A:负责下载文件
    2. 协程B:处理用户输入
    3. 协程C:写入日志文件
  2. 在协程的模型中,这些任务(协程ABC)的调度和切换都是由程序员控制的,因此可以灵活地按照需求来安排它们的执行方式
    1. 可以设定一个循环,每隔一段时间轮流执行ABC,比如先执行一些下载任务(A),然后检查用户输入(B),最后将产生的数据写入日志(C
    2. 可以在一次操作中完全完成A,再进入B,最后进入C。比如,下载一个文件后才处理用户输入,然后再写日志
    3. 可以根据需求随时在AB之间切换,并在需要时执行C。比如,当AB有等待操作(如网络延迟或用户未输入)时,立即切换到其他任务执行

示例代码

  1. 模拟一些任务
    1. 任务A:下载文件(模拟为延迟操作)
    2. 任务B:处理用户输入(模拟为打印输出)
    3. 任务C:写入日志(模拟为打印日志内容)
    4. 为了简单起见,示例代码使用 std::chrono::milliseconds 模拟延迟,以便切换任务

  1. 代码解析如下:
  2. 为什么上面的Timerco_await 类型
    1. Timer 之所以能被 co_await,是因为它符合协程机制中 awaitable 的要求
    2. 为了让一个对象可以使用 co_await 操作符,C++20 协程的设计中规定了该对象需要实现特定的接口或语义
    3. Timer 就是这样一个“可等待对象”(awaitable),它实现了协程等待的必要接口,因此可以被 co_await 使用
  3. Timer 如何成为 awaitable
    1. C++20 协程模型中,一个对象要成为 awaitable 类型,通常需要实现以下三个方法:
    2. await_ready:返回 bool 类型,用于判断是否已经准备好
      如果返回 true,则协程不暂停,继续执行
      如果返回 false,协程会暂停等待
    3. await_suspend:在协程挂起时被调用
      它接受一个 std::coroutine_handle 参数,这个句柄表示当前协程的句柄,可以在挂起时保存以便之后恢复
      Timer 中,我们通过 std::this_thread::sleep_for 模拟等待一段时间后调用 handle.resume(),恢复协程
    4. await_resume:当协程恢复执行时调用,用于返回等待操作的结果
      这里我们不需要返回特定结果,因此它只是一个空函数
  4. co_await
    1. co_await 并不是一种类型,而是一种 操作符,用来在协程中表示“等待”操作
    2. 它的作用是使得协程在执行到这个点时 可以暂停执行,等待某个条件达成后再恢复
  5. 协程任务对象
    1. C++20 协程中,任务的返回类型需要一个 promise type,这个类型是协程的状态和结果的承载体
    2. promise_type 必须实现特定的方法,以定义协程的行为
    3. get_return_object():这个方法返回协程的返回对象(通常是一个 Task 对象)
      当协程完成时,这个返回对象将被返回给调用者
    4. initial_suspend():这个方法返回一个对象,表示协程开始时是否要挂起
      返回 std::suspend_always 表示协程一开始就挂起,返回 std::suspend_never 表示立即开始执行
    5. final_suspend():这个方法在协程完成时被调用,返回的对象决定了协程结束后是否要挂起
      一般来说,返回 std::suspend_always 可以让协程在结束时仍然保持状态,便于后续处理
    6. return_void():用于处理无返回值的协程,表示协程正常结束
    7. unhandled_exception():处理未捕获异常的方式。通常可以选择终止程序或以其他方式处理异常

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2024-12-31
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享