核心内容摘要
青春协奏曲:当男生女生一起,奏响心动的旋律
菜鸟要知道的「线程安全」本文基于go技术栈进行解释相关概念及部分源码展示~✨线程安全是什么东西线程安全就是在多个线程并发操作同个资源的时候产生的数据始终一致。
下面这个就是就是一个线程安全问题的例子var count int var wg sync.WaitGroup func main() { wg.Add(
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实际可能是
1950 等任意值 } //count 不是原子操作读 → 加1 → 写多个 goroutine 交错执行会导致丢失更新。
解决办法有很多最常见的就是加锁同一时间只允许有一个线程可以对资源进行操作。
详细的代码在此不再作再多展示因为我们本次的目的是研究为什么加锁可以保证线程安全✨为什么加锁可以保证线程安全?吾日三省吾身⚠️是什么决定了多线程会出现不安全的情况⚠️加锁实际上是如何保证线程安全⚠️加锁转成底层指令会是怎么样我们把问题拆解逐步分析再重新思考是什么决定了多线程会出现不安全的情况对资源的并发操作比如一个简单修改变量的操作转化成底层的汇编指令时会生成多个指令。
汇编指令读取-计算-写入 0x0000 MOVQ .counter(SB), AX ; 读取当前值 0x0007 LEAQ 1(AX), CX ; 计算新值 (AX
0x000b MOVQ CX, .counter(SB) ; 写回新值如果不加锁的情况下就会出现以下情况协程1协程2协程3读取a1读取a1读取a1a写入a2aa写入a2写入a2解决语言层面加入锁去确保多线程时同一时间只允许其中一个线程对资源进行内存可见性这里篇幅稍微涉及到的知识可能会稍微有点多请耐心阅读在多核CPU的架构下进程内的不同线程可能会被运行在不同的CPU核心中。
N个核心就代表这个CPU可以同时运行N个线程。
️ 现代计算机的存储层次结构从快到慢CPU 寄存器 ↓ L1 缓存每个核心私有1ns ↓ L2 缓存每个核心私有或共享3ns ↓ L3 缓存所有核心共享10ns ↓ 主存 RAM100ns ↓ 磁盘 / 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 或 InvalidCore 0 会广播 “Invalidate” 消息Core 1 收到 Invalidate 后将自己缓存中x的副本标记为Invalid (I)下次读x时发现缓存行无效 → 触发 cache miss → 从其他核心的缓存或主存加载最新值优先从其他核心读取。
别重排我屏障前后的代码这里涉及到的是「指令重排」的问题因为一般情况下编译器会自己优化编排命令的执行顺序。
a : true b : go func() { b msg a false }() for a { } println(b) //打印结果有可能为空 //原因:编译器对指令进行了优化重新编排bmsg被安排在afalse之后这里可以理解为晚上去大排档吃夜宵你点了一份炒面另外有两个客人点了两份炒粉。
老板可能会优先把两份炒粉一起炒了先再安排炒面。
但是老板也有可能是个守规矩的人先把你的炒面炒了再给其他两个客人炒粉。
所以最终的结果取决于老板当时的想法。
✨
总结线程安全是什么确保多个线程访问同个资源最终结果的一致性。
怎么保证线程安全加锁依赖内存屏障。
保证同一资源同时操作的线程只有一个和解决「内存可见性」「指令重排问题