当前位置:首页 > 技术学院 > 技术前线
[导读]很多开发者提到多线程线程安全,第一反应就是给共享变量加互斥锁,但是在高并发高性能场景下,互斥锁的性能开销非常可观:大量线程竞争锁的时候,会触发频繁的内核态上下文切换,吞吐率大幅下降,延迟飙升。无锁编程就是在完全不使用互斥锁的前提下,通过硬件级别的原子操作和精巧的并发逻辑设计,实现线程安全,全程不会阻塞任何线程,性能比加锁方案高出数倍。彻底理清这套技术体系的核心思路,你就能在高并发场景下写出极致性能的线程安全代码。

很多开发者提到多线程线程安全,第一反应就是给共享变量加互斥锁,但是在高并发高性能场景下,互斥锁的性能开销非常可观:大量线程竞争锁的时候,会触发频繁的内核态上下文切换,吞吐率大幅下降,延迟飙升。无锁编程就是在完全不使用互斥锁的前提下,通过硬件级别的原子操作和精巧的并发逻辑设计,实现线程安全,全程不会阻塞任何线程,性能比加锁方案高出数倍。彻底理清这套技术体系的核心思路,你就能在高并发场景下写出极致性能的线程安全代码。

一、无锁编程的核心基石:硬件原生支持的原子操作

无锁编程的底层基础,是CPU硬件直接提供的原子操作指令,这类指令可以在一个完整的硬件周期里完成对内存的读写修改操作,执行过程中绝对不会被其他线程的中断打断,天然就具备线程安全性。完全不需要内核态的锁机制介入,全程在用户态就可以完成操作,性能开销只有几纳秒,比无竞争场景下的互斥锁还要快几十倍。

C++标准库封装的std::atomic原子类型,就是对硬件原子操作的跨平台抽象。你只需要把原本的普通共享变量,替换成对应的原子类型,调用它内置的fetch_add、store、load等方法,就可以直接实现线程安全的自增、赋值、读取操作,完全不需要加任何锁。比如多个线程同时对一个原子计数器做自增操作,最终的结果永远是正确的,不会出现普通int变量多线程自增时的数值丢失问题。

你还可以通过指定不同的内存序参数,在保证线程安全的前提下进一步优化性能。最宽松的内存序只保证操作本身的原子性,不做任何额外的内存屏障同步,适合只需要原子计数、不需要同步其他内存数据的场景,性能可以做到极致。而最严格的顺序一致内存序,会保证所有线程看到的原子操作顺序和代码编写顺序完全一致,逻辑最不容易出错,适合对内存可见性要求极高的场景。

所有无锁编程的逻辑,全部建立在原子操作的基础之上,不需要任何内核锁的介入,全程不会让线程进入睡眠阻塞状态,哪怕多个线程同时竞争原子变量,也只会在用户态做少量的重试循环,完全不会触发昂贵的上下文切换。

二、CAS循环:构建复杂无锁数据结构的核心技术

CAS也就是比较并交换操作,是所有复杂无锁数据结构的核心实现技术。它的逻辑非常简单:执行操作的时候,先检查内存里当前的值是不是和预期的值完全相等,如果相等,就把内存里的值更新成新的值;如果不相等,说明在操作执行的间隙,其他线程已经修改过这个值了,本次操作直接失败,返回当前内存里最新的实际值。

实际使用的时候,我们会把CAS操作放在一个循环里:先把当前内存的最新值读出来,基于这个值计算出我们想要更新的新值,然后执行CAS操作尝试更新。如果CAS执行成功,说明整个更新过程没有被其他线程干扰,操作直接完成退出循环。如果CAS执行失败,说明其他线程抢先修改了数据,我们就用刚返回的最新值重新计算新值,再次尝试CAS更新,直到最终操作成功为止。

这个重试循环的模式,是实现无锁栈、无锁队列这些复杂数据结构的基础。比如实现一个无锁栈的入栈操作,我们先读取当前栈顶指针的最新值,把新节点的next指针指向这个栈顶节点,然后用CAS操作尝试把栈顶指针直接替换成新节点。如果替换成功,入栈操作就完成了;如果替换失败,说明其他线程已经修改了栈顶指针,我们就重新读取最新的栈顶指针,再次尝试入栈,直到成功为止。整个过程完全不需要加任何锁,多个线程可以同时并发执行入栈操作,全程没有任何线程被阻塞。

无锁编程里最经典的ABA问题,也有非常成熟的解决方案。当一个指针的值先从A变成B,再改回A的时候,CAS操作会误以为指针从来没有被修改过,导致逻辑出错。我们可以用带版本号的标记指针来解决这个问题,把一个64位的变量拆分成两部分,低48位用来存储指针的地址,高16位用来存储版本号,每次修改指针的时候,同时把版本号加1。哪怕指针的地址值变回了之前的A,版本号也已经不一样了,CAS操作可以轻松识别出指针已经被修改过,彻底避免ABA问题。

三、无锁设计的工程化思路与落地边界

除了直接基于原子操作实现无锁数据结构之外,工业界还有很多更简单的无锁设计思路,不需要复杂的CAS循环,就能在业务场景里轻松实现线程安全。最经典的思路就是消除共享可变状态,把原本需要多线程同时修改的共享数据,拆分成每个线程独立的私有副本。比如多线程场景下的统计计数器,不需要所有线程同时竞争同一个全局计数器,而是每个线程维护自己本地的独立计数器,线程只修改自己的私有副本,完全没有任何竞争。最终汇总结果的时候,把所有线程的本地计数器加起来得到总值,全程不需要任何原子操作和锁,性能比原子计数器还要高几个数量级。

还有一个非常常用的思路是单线程串行化,把所有对共享状态的修改操作,全部投递到一个专属的工作线程的无锁任务队列里,由这个单线程串行处理所有修改请求。其他线程只能投递任务,不能直接修改共享状态,所有修改天然就是串行执行的,完全不会出现数据竞争,不需要任何锁。很多高性能网络服务器的连接状态管理逻辑,全部都是用这种模式实现的,稳定性极高,性能也非常出色。

当然无锁编程也不是银弹,它的实现复杂度非常高,调试难度极大,普通开发者手写无锁数据结构很容易引入隐蔽的并发Bug。工业界生产环境里,优先选择经过大量场景验证的标准库无锁实现,不要从零手写复杂的无锁结构。同时要明确无锁的适用边界,只有在高并发、锁竞争非常激烈的场景下,无锁的性能优势才能体现出来,如果并发度很低,锁几乎没有竞争,互斥锁的实现简单、稳定性高,远比无锁方案更适合。

无锁编程的核心本质,就是用精巧的并发逻辑设计,完全绕开互斥锁的阻塞开销,把多线程的性能潜力发挥到极致,是高并发高性能服务器开发里的核心进阶技术。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭