上篇聊了条件变量。今天聊多线程的最后一个基础话题——原子操作。前面讲的互斥锁、条件变量都是通过锁来保证线程安全。但锁是有开销的——加锁解锁涉及系统调用、线程调度、上下文切换。如果临界区很小比如只是一个计数器加一锁的开销可能比实际操作还大。原子操作就是来解决这个问题的不用锁也能保证线程安全。什么是原子操作原子操作就是不可分割的操作——要么完全做完要么完全没做不存在做到一半被其他线程看到中间状态的情况。// 非原子操作多线程下不安全 int counter 0; void increment() { counter; // 读-改-写三步不是原子的 // 实际执行1.读counter 2.1 3.写回counter // 两个线程同时做可能丢失一次更新 } // 原子操作线程安全 atomicint atomicCounter{0}; void atomicIncrement() { atomicCounter; // 一条CPU指令完成不可分割 }atomic在底层通常对应一条CPU指令——x86上的lock inc指令。这条指令执行期间其他核心无法访问这个内存地址。所以不需要操作系统层面的锁性能比mutex快得多。atomic的基本用法atomicint count{0}; atomicbool running{true}; atomicdouble temperature{0.0}; // C20起支持浮点原子 // 基本操作 count.store(42); // 写入 int val count.load(); // 读取 count.fetch_add(1); // 原子加返回旧值 count; // 同上 count 5; // 原子加5 // 比较并交换CAS——无锁编程的核心 int expected 10; bool success count.compare_exchange_strong(expected, 20); // 如果count10把count改成20返回true // 如果count!10把expected更新为count的当前值返回falseCASCompare-And-Swap是无锁编程的核心操作。它的逻辑是如果内存位置的值等于预期值就更新为新值否则什么都不做。整个过程是原子的。很多无锁数据结构都是基于CAS实现的。比如无锁栈的push操作templatetypename T class LockFreeStack { struct Node { T data; Node* next; }; atomicNode* head_{nullptr}; public: void push(const T data) { Node* newNode new Node{data, head_.load()}; while (!head_.compare_exchange_weak(newNode-next, newNode)) { // 如果head变了newNode-next被更新为最新的head // 然后重试 } } };内存序原子操作的隐藏考点原子操作有一个容易被忽略的参数——内存序memory order。它决定了原子操作和其他内存操作之间的可见性顺序。C定义了六种内存序// 最严格顺序一致性默认值 count.store(42, memory_order_seq_cst); // 最宽松不保证和其他操作的顺序 count.store(42, memory_order_relaxed); // 释放-获取配对写端用release读端用acquire count.store(42, memory_order_release); // 写端 int val count.load(memory_order_acquire); // 读端 // release之前的写操作对acquire之后的读操作可见默认的seq_cst最安全但也最慢。relaxed最快但不能保证操作顺序。release-acquire配对是中间地带——保证配对点前后的可见性但不保证全局顺序。说实话内存序这个话题非常复杂涉及到CPU的指令重排、缓存一致性协议、内存屏障这些底层概念。面试里一般不会考得太深但你要知道有这回事以及默认的seq_cst在大部分情况下就够用了。在机器人开发中的应用在机器人系统里原子操作最常用于状态标志和计数器class RobotController { atomicbool emergencyStop_{false}; atomicint frameCount_{0}; public: void onEmergencySignal() { emergencyStop_.store(true, memory_order_release); } bool shouldStop() { return emergencyStop_.load(memory_order_acquire); } void onNewFrame() { frameCount_.fetch_add(1, memory_order_relaxed); } int getFrameCount() { return frameCount_.load(memory_order_relaxed); } };emergencyStop用release-acquire配对确保设置之后所有线程都能立即看到。frameCount用relaxed就够了——帧计数不需要严格的可见性保证大概准确就行。顺便提一句C20引入了atomic_shared_ptr让shared_ptr的引用计数操作可以用原子方式完成。之前shared_ptr的引用计数虽然本身是原子的但如果你想原子地替换整个shared_ptr指向的对象就得自己加锁。现在有了atomic_shared_ptr这个操作也是无锁的了。面试中的关键考点原子操作能完全替代mutex吗不能。原子操作只适合简单的操作读写一个变量、加减。如果临界区涉及多个变量的复杂操作还是得用mutex。atomic的是线程安全的吗是的。atomic的所有操作都是原子的。但注意如果是atomic自定义类型默认不提供原子操作需要自己实现。无锁编程一定比加锁快吗不一定。无锁编程在竞争不激烈的时候很快但在高竞争场景下CAS反复重试的开销可能比mutex还大。另外无锁代码更难写、更难调试。原子操作的性能对比实际测一下原子操作和mutex的性能差距。在一个简单的计数器场景下10个线程同时对计数器加一各加100万次mutex版本耗时约85毫秒atomic版本耗时约12毫秒快了七倍左右。但如果把操作换成读-判断-写的复合逻辑比如只有计数器为偶数时才加一atomic版本需要用CAS循环重试性能优势就缩小到两倍左右。所以选择的时候不要盲目追求无锁——简单的高频操作用atomic复杂的临界区还是老老实实用mutex。面试的时候能给出这种有数据支撑的分析比空谈理论有说服力得多。再补充一个面试加分点std::atomic_flag的用法。它是C中最轻量的原子类型保证lock-free常用于实现自旋锁。在机器人控制系统里有些临界区特别小比如翻转一个bool标志位用mutex太重了用atomic_flag实现的自旋锁反而更合适。写法是用test_and_set尝试获取锁用clear释放锁。不过要注意自旋锁会占用CPU只适合临界区极短的场景。面试时如果你能对比mutex、atomic、atomic_flag三种方案的适用场景面试官会觉得你对无锁编程的理解很全面。给正在准备面试的你原子操作在面试里不算高频考点但经常作为多线程话题的延伸被问到。知道atomic的基本用法了解CAS操作能讲清楚什么时候用原子操作、什么时候用mutex面试就足够了。内存序如果面试官不追问不用主动展开——太深了大部分人讲不清楚。下篇聊线程池设计——机器人系统中的标配组件。前面讲的都是手动创建线程但实际项目里线程的创建和销毁是有开销的线程池就是用来复用线程的。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第56篇 多线程之条件变量——生产者消费者模型在传感器数据中的应用 下一篇预告第58篇 多线程之线程池设计——机器人系统中的标配组件有任何问题欢迎评论区留言我会尽量回复。