C#线程底层原理知识

前言:底层原理与上层知识对应关系

上位机要用的技术 (清单内容) 对应的深度知识 (为什么学这个) 实际解决的上位机痛点
async / await 异步状态机 / 上下文切换 保证你一边读取 PLC 数据,一边操作 UI 界面,界面绝不假死
Interlocked (原子操作) 内存模型 / 指令重排 在高速流水线计数时,保证数据一个都不丢 ,不需要沉重的 lock
ConcurrentCollections CAS 无锁编程 / 工作窃取 多个相机、传感器同时往缓存写数据时,程序不会崩溃,性能最高。
SemaphoreSlim 内核模式 vs 用户模式锁 控制有限的硬件连接数(如 PLC 只有 4 个连接口),实现资源排队
CancellationToken 线程协作机制 当按下"紧急停止"按钮时,能优雅且瞬间关闭所有后台扫描线程。

一内存模型与可见性

1.硬件知识

  • 主内存(内存条) 所有全局变量、静态变量,真实原始数据都存在这里,速度最慢。

  • CPU 高速缓存(L1/L2/L3 缓存) 每个 CPU 核心自带小仓库,速度极快,比内存条快几十上百倍。线程跑在 CPU 核心上,优先读写自己的缓存,不爱读慢速的内存条。

  • 线程一个 CPU 核心同一时间跑一个线程;多线程 = 多个核心同时干活,或一个核心快速切换。

2.什么是 计算机内存模型

1.定义

内存模型 = 一套规则

规定

CPU、缓存、主内存之间,数据怎么读、怎么写、指令怎么排序、数据什么时候同步

2.为什么需要

CPU 为了跑得快,会私自做两件事优化性能:

  1. 缓存隔离:变量先存在自己缓存,不实时同步到内存
  2. 指令重排:打乱代码执行顺序,乱序执行提升效率

这套乱搞的规则 + 数据同步规则,合起来就是内存模型

3.数据同步规则

系统默认是没有同步规则的

规则 / 机制 对应工具 解决的核心问题
1. 原子性 Interlocked 保证 count++ 这类操作不可拆分,要么全成,要么全不成
2. 互斥访问 lock / Monitor / SemaphoreSlim 同一时间只允许一个线程进入临界区,避免同时修改
3. 可见性 volatile / 内存屏障 保证一个线程对变量的修改,其他线程能立刻看到
4. 禁止指令重排 volatile / lock 自带内存屏障 防止编译器 / CPU 为了优化打乱执行顺序,导致半初始化对象被读取

4.什么是 可见性

场景举例

有一个全局变量:

cs 复制代码
static bool flag = false;
  • 线程 A:循环判断 flag,只要是 false 就一直跑
  • 线程 B:隔一段时间,把 flag = true

理想情况(正常人理解)

B 改了 flag → 内存立刻更新 → A 马上读到新值 → 循环结束。

现实 CPU 情况(出问题的根源)

  1. 线程 A 启动,把 flag主内存 读到自己 CPU 缓存
  2. 之后 A 一直只读自己缓存里的旧值:false
  3. 线程 B 修改 flag = true,只改了B 自己的缓存
  4. B没有立刻把数据写回主内存
  5. 主内存还是旧值,A 的缓存也永远是旧值

👉 最终结果:B 明明修改了变量,A 完全看不见,程序死循环卡死

✅ 这就叫:可见性问题

可见性:一个线程对共享变量的修改,能不能被其他线程及时看到

5.为什么会出现【不可见】

1.每个CPU有独立缓存,互相隔离

2.线程默认优先读写本地缓存,不频繁读写慢速主内存

3.写操作不会立刻刷新到主内存,缓存和内存数据不一样

3.C#里怎么解决可见性

1. volatile 关键字(专门解决可见性)

作用:

  • 读:强制每次都从主内存读最新数据,不读缓存
  • 写:强制修改立刻刷回主内存
  • 禁止 CPU 随便打乱代码顺序

加上之后:B 改完变量立刻同步到内存,A 下一次读取直接拉内存最新值,互相可见

注意:volatile 只管「看得见」,不管 i++ 这种多步操作的安全(原子性)。

2. lock / Monitor

加锁会自动:

  • 进入锁:刷新缓存,读取最新内存数据
  • 退出锁:把修改强制写回主内存所以 lock 自带解决可见性,功能更强。

二指令重排

CPU 为了提速,会打乱代码执行顺序

单线程没事,多线程下会出现逻辑错乱volatile 同时会禁止指令重排,保证代码执行顺序不乱。

三线程同步语

三个层次 ,由低到高

原子操作 → 内存屏障 (volatile) → 互斥锁 (lock)

层级越低,越好理解、越贴近硬件;层级越高、功能越强、越慢

第一层:原子操作(硬件层级:最轻量)

1. 是什么

CPU 硬件直接支持的不可拆分 操作。一句话:要么做完,要么没做,不会做一半被抢走。

2. 能干啥

只解决一个问题:原子性 比如 i++ 本来是三步:1. 读值 → 2. 计算 → 3. 写回容易被其他线程插队改错。

用原子操作(C# Interlocked):

cs 复制代码
Interlocked.Increment(ref count);

硬件保证:这一整步绝不被打断

3. 特点

  • 纯硬件指令,速度最快
  • 能力最弱:✅ 保证原子性❌ 不保证可见性❌ 不保证代码执行顺序

第二层:内存屏障 /volatile(软件 + 硬件层级)

1. 是什么

给 CPU 下「强制命令」:

  1. 不许乱序执行代码(禁止指令重排)
  2. 不许私自缓存变量,强制读写主内存

2. 能干啥

解决两个问题:可见性 + 有序性

  • 可见性:A 线程改完变量,B 线程立刻能看到最新值
  • 有序性:代码按你写的顺序跑,CPU 不能偷偷调换

3. 对应 C#

volatile 关键字、内存屏障 MemoryBarrier

4. 特点

  • 无阻塞、无等待、速度中等
  • 短板明显:✅ 保证可见性、有序性❌ 不保证原子性 依然挡不住 count++ 并发错乱

第三层:互斥锁(操作系统层级・最重)

1. 是什么

最通俗:抢厕所模式 一把锁同一时间只允许一个线程进去执行代码,其他人排队等着。

C# 对应:lockMonitor、Mutex

2. 能干啥

全包解决三大问题

  1. 原子性:临界区代码整块执行,不被插队
  2. 可见性:加锁 / 解锁自动刷新内存缓存
  3. 有序性:锁范围内指令禁止乱重排

3. 特点

  • 功能最强、最安全
  • 代价最大:会阻塞线程、上下文切换、速度最慢

总结

层级 代表技术 解决问题 性能
1 原子操作 Interlocked 原子性 最快
2 内存屏障 volatile 可见性、有序性 中等
3 互斥锁 lock / Monitor 原子 + 可见 + 有序 最慢

四异步状态机

1.什么是机

这里的 "机",不是 "机器" 那个冷冰冰的意思,而是 **"机制 / 装置 / 系统"** 的简称,英文是 Machine,指的是一套能自动运转、按规则干活的东西。就像:

  • 蒸汽机:靠蒸汽推动的装置
  • 发动机:靠燃料推动的装置
  • 状态机:靠 "状态" 来驱动运转的装置

合起来,状态机(State Machine) 就是:

一套以状态为核心,根据触发条件在不同状态之间切换,并在每个状态下执行固定行为的自动化机制。

异步状态机 ,就是把一段可暂停、可恢复 的代码流程,拆成多个 "状态",靠状态切换来管理异步执行(不卡线程、等待 IO / 耗时操作)。C# 里 async/await 的底层本质就是编译器自动生成的异步状态机

2.基本概念

1. 状态机(State Machine)

  • 一个对象 / 流程,只有有限个状态(如:未开始、运行中、等待中、完成、出错)
  • 触发条件(事件、await、信号)从一个状态跳到另一个
  • 每个状态下有固定行为

生活例子:微波炉

  • 状态:关门待机 → 启动 → 加热中 → 暂停 → 加热完成 → 开门
  • 触发:按 "开始"、定时到、开门、按暂停

2. 异步(Async)

  • 不阻塞当前线程:等网络、文件、数据库时,线程去干别的,不傻等
  • 完成后再回来继续执行

3. 异步状态机

  • 把异步方法切成多个 "片段" (每个 await 切一刀)
  • 用状态记录 "执行到哪了"
  • 支持:暂停 → 保存现场 → 等待 → 恢复现场 → 继续跑
  1. = 一个自动干活的小工具 / 小机器
  2. 状态 = 记录你代码跑到哪一步
    • 状态 - 1:还没开始
    • 状态 0:刚跑完 await 前面
    • 状态 - 2:全部跑完结束
  3. 异步 = 等待的时候不卡界面、不卡线程

合起来:异步状态机 = 专门帮 async 方法记住进度、暂停、后续接着跑的后台小工具

五工作窃取

  • 传统线程池 :大家共用一个任务队列。
    • 一个线程干完活,去公共队列取 → 大家抢同一个队列,锁竞争大
    • 有的线程忙死,有的闲死 → CPU 利用率低
  • 工作窃取线程池 (如 Java ForkJoinPool、.NET TaskScheduler 底层):
    1. 每个线程有自己的私有任务队列(双端队列 Deque)。
    2. 线程优先从自己队列的 "尾部" 拿任务(LIFO,自己的活自己先干)。
    3. 自己队列为空 → 去偷别人队列 "头部" 的任务(FIFO)。
    4. 头尾分离 :自己从尾拿、别人从头偷 → 几乎不冲突、不用锁(CAS)

总结:工作窃取 = 线程自带私队 + 自己干完偷别人 + 头尾分离少竞争 → 负载均衡、CPU 拉满

六上下文切换

1.定义

上下文切换 :CPU 现在正在跑「线程 A」,临时暂停、保存 A 的所有数据,切去跑「线程 B」,等下再切回来继续跑 A,这个来回切的过程,就叫上下文切换


2. 什么是「上下文」?

就是这个线程当下的全部现场数据

  • 寄存器值
  • 程序执行到哪一行
  • 栈数据、状态标记

好比:你写作业写到一半,把笔放下、本子合上、记好写到第几题 去洗碗,洗完再翻开本子、接着刚才位置写。👉 合上 + 记录 + 切换干活 = 上下文切换


3. 为什么会发生上下文切换?

三种最常见情况:

  1. 时间片用完CPU 公平起见,每个线程只给一小段时间,时间到强制切走。
  2. 线程阻塞 / 等待 比如 Thread.Sleep、等待 IO、锁等待、await 挂起,线程没事干,CPU 切去跑别的线程。
  3. 主动让步 代码主动 Thread.Yield 让出 CPU。

4. 关键重点(必记)

① 上下文切换 = 有开销(费性能)

保存现场 + 恢复现场 + 切换调度,要耗 CPU、耗时间

  • 切换越少 → 程序越快
  • 疯狂频繁切换 → 性能暴跌、卡顿

② 两种切换(简单区分)

  1. 用户态切换:只切线程,开销小
  2. 内核态切换 :进系统内核操作,开销很大(尽量避免)

七真假异步

相关推荐
AC赳赳老秦1 小时前
OpenClaw与Excel联动:批量读取/写入数据,生成可视化报表
开发语言·python·excel·产品经理·策略模式·deepseek·openclaw
code_whiter1 小时前
C++9(vector)
开发语言·c++
xieliyu.1 小时前
Java手搓数据结构:从零模拟实现单向无头非循环链表
java·数据结构·学习·链表
覆东流1 小时前
第5天:Python字符串操作进阶
开发语言·后端·python
吴梓穆1 小时前
UE5 C++ 使C++创建动画蓝图
开发语言·c++·ue5
冰暮流星1 小时前
javascript之表单事件1
开发语言·前端·javascript
0xDevNull1 小时前
队列(Queue)实战教程:从原理到架构应用
java·开发语言·后端
ShineWinsu2 小时前
C++技术文章
开发语言·c++
再写一行代码就下班2 小时前
word模版导出(占位符方式)
java·开发语言·word