news 2026/4/23 20:17:54

菜鸟要知道的「线程安全」

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
菜鸟要知道的「线程安全」

菜鸟要知道的「线程安全」

🤫本文基于go技术栈进行解释相关概念及部分源码展示~

✨线程安全是什么东西

线程安全,就是在多个线程并发操作同个资源的时候,产生的数据始终一致

下面这个就是就是一个线程安全问题的例子:

var count int var wg sync.WaitGroup func main() { wg.Add(2) go func() { defer wg.Done(); for i := 0; i < 1000; i++ { count++ } }() go func() { defer wg.Done(); for i := 0; i < 1000; i++ { count++ } }() wg.Wait() fmt.Println(count) // 期望 2000,实际可能是 1800、1950 等任意值 } //count++ 不是原子操作(读 → 加1 → 写),多个 goroutine 交错执行会导致丢失更新。

解决办法有很多,最常见的就是加锁,同一时间只允许有一个线程可以对资源进行操作。
详细的代码在此不再作再多展示,因为我们本次的目的是研究:为什么加锁可以保证线程安全?

✨为什么加锁可以保证线程安全?

🧐吾日三省吾身

  • ⚠️是什么决定了多线程会出现不安全的情况?
  • ⚠️加锁实际上是如何保证线程安全?
  • ⚠️加锁转成底层指令会是怎么样?

🧐我们把问题拆解,逐步分析,再重新思考

🎯是什么决定了多线程会出现不安全的情况?
📔对资源的并发操作

🌰比如:一个简单修改变量的操作,转化成底层的汇编指令时,会生成多个指令。

汇编指令:读取-计算-写入 0x0000 MOVQ "".counter(SB), AX ; 读取当前值 0x0007 LEAQ 1(AX), CX ; 计算新值 (AX + 1) 0x000b MOVQ CX, "".counter(SB) ; 写回新值

如果不加锁的情况下,就会出现以下情况:

协程1协程2协程3
读取a=1读取a=1读取a=1
a++
写入a=2a++
a++
写入a=2写入a=2

解决:语言层面加入锁去确保多线程时同一时间只允许其中一个线程对资源进行

📔内存可见性

这里篇幅稍微涉及到的知识可能会稍微有点多,请耐心阅读!

在多核CPU的架构下,进程内的不同线程可能会被运行在不同的CPU核心中。
N个核心就代表这个CPU可以同时运行N个线程。

🖥️ 现代计算机的存储层次结构(从快到慢)

CPU 寄存器 ↓ L1 缓存(每个核心私有,~1ns) ↓ L2 缓存(每个核心私有或共享,~3ns) ↓ L3 缓存(所有核心共享,~10ns) ↓ 主存 RAM(~100ns) ↓ 磁盘 / SSD

看到这里就可以想到一个糟糕的事情:

线程A修改了变量a,但是一般情况下为了加快执行效率
CPU不会每次把数据写到L3或RAM中,而是会先写入L2或L1缓存中,然后发送失效广播给其他核心。(不保证马上处理)
所以运行在其他核心的线程读取变量a时,可能读到的是旧值,或者新值。

这会导致一个问题,只有当前线程或执行在该核心的线程能保证百分百读到更新后的值。

📔内存屏障

这里引入了一个「内存屏障」的概念,去解决内存可见性问题



内存屏障是“同步指令”,作用类似于操作数据库开始事务的命令,执行了内存屏障指令后,后续的指令会被内存屏障指令影响,功能可以分为三点:

  • 告诉 CPU:“把你缓存里的脏数据刷到主存!”
  • 告诉其他 CPU:“我刚刚更新了数据A,请把你们的数据A标记为过期!”
  • 告诉编译器/CPU:“别重排我屏障前后的代码!”

让更新了数据的CPU的脏数据刷到主存!

这里就是刚刚说到的,CPU操作数据时,不会直接把数据更新回主存,而是直接操作CPU的三级缓存,因为这样效率更高。
最后如果缓存数据满了,会采用淘汰算法,把淘汰的脏数据刷回主存中。

这里大家可能会有一个疑问:

❓为什么不直接更新到L3缓存就可以了?反正L3是所有核心的共享缓存。

现代的服务器一般都会有多个CPU插槽,每个CPU之间的L3是不可以互相访问的,所以要把更改的数据刷回主存才能让所有的CPU能找到最新的数据。

我刚刚更新了数据A,请把你们的数据A标记为过期!

当核心0修改了数据后并启用了内存屏障命令

mov [x], 1 ; 写入 x = 1(先写入 Core0 的 store buffer / cache) mfence ; 内存屏障

缓存一致性协议(如 MESI)被触发

  • Core 0 的缓存行状态变为Modified (M)
  • 如果其他核心(如 Core 1)的缓存中有x的副本(状态为 Shared 或 Invalid),
    Core 0 会广播 “Invalidate” 消息

Core 1 收到 Invalidate 后

  • 将自己缓存中x的副本标记为Invalid (I)
  • 下次读x时,发现缓存行无效 → 触发 cache miss → 从其他核心的缓存或主存加载最新值(优先从其他核心读取)

别重排我屏障前后的代码!

这里涉及到的是「指令重排」的问题
因为一般情况下,编译器会自己优化编排命令的执行顺序。

a := true b := "" go func() { b = "msg" a = false }() for a { } println(b) //打印结果有可能为空 //原因:编译器对指令进行了优化重新编排b="msg"被安排在a=false之后

这里可以理解为:
晚上去大排档吃夜宵,你点了一份炒面,另外有两个客人点了两份炒粉。老板可能会优先把两份炒粉一起炒了先,再安排炒面。
但是老板也有可能是个守规矩的人,先把你的炒面炒了,再给其他两个客人炒粉。
所以最终的结果取决于老板当时的想法。

✨总结

线程安全是什么?

确保多个线程访问同个资源最终结果的一致性。

怎么保证线程安全?

加锁&依赖内存屏障。保证同一资源同时操作的线程只有一个和解决「内存可见性」&「指令重排问题

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 13:03:46

数据库索引决策与优化提示

简介&#xff1a;本文档深入剖析数据库优化器&#xff08;Optimizer&#xff09;的工作原理&#xff0c;解释全表扫描优于索引的场景&#xff0c;并详细列出了强制数据库使用特定索引的语法&#xff08;Index Hints&#xff09;。1. 核心原理&#xff1a;数据库是如何做决策的&…

作者头像 李华
网站建设 2026/4/23 13:04:18

计算机毕业设计之基于Python的高校新冠疫情防控信息管理系统

本系统为用户而设计制作高校新冠疫情防控信息管理系统&#xff0c;旨在实现高校新冠疫情防控信息智能化、现代化管理。本高校新冠疫情防控信息管理自动化系统的开发和研制的最终目的是将高校新冠疫情防控信息管理的运作模式从手工记录数据转变为网络信息查询管理&#xff0c;从…

作者头像 李华
网站建设 2026/4/23 12:46:32

计算机毕业设计之jsp基于web的加油站管理系统

系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对加油站管理的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上”的思想…

作者头像 李华
网站建设 2026/4/23 13:01:47

一文讲透|一键生成论文工具 千笔写作工具 VS 文途AI 本科生必备神器

随着人工智能技术的迅猛迭代与普及&#xff0c;AI辅助写作工具已逐步渗透到高校学术写作场景中&#xff0c;成为本科生完成毕业论文不可或缺的辅助手段。越来越多面临毕业论文压力的学生&#xff0c;开始依赖各类AI工具简化写作流程、提升创作效率。但与此同时&#xff0c;市场…

作者头像 李华
网站建设 2026/4/23 14:50:23

从Bug猎人到产品决策者:测试员的权力重构

测试员角色的时代嬗变 在传统软件开发生命周期中&#xff0c;测试员常被定位为“Bug猎人”——专注于缺陷发现与报告&#xff0c;却游离于核心决策之外。然而&#xff0c;随着敏捷开发、DevOps及AI驱动的质量保障体系普及&#xff0c;测试员的角色正经历深刻重构。从被动执行者…

作者头像 李华
网站建设 2026/4/23 16:28:50

鹤岗启示录:资源枯竭城市的AI测试产业重生

资源诅咒下的破局契机 当煤炭资源濒临枯竭&#xff0c;鹤岗面临GDP下滑60%、人口外流超20万的城市困局。传统产业转型的迫切需求与AI测试产业低门槛、高赋能特性形成战略契合点。本文通过拆解鹤岗构建区域性AI测试枢纽的三年实践&#xff0c;为测试从业者提供资源受限场景下的…

作者头像 李华