上篇聊了条件变量。今天聊多线程的最后一个基础话题——原子操作。

前面讲的互斥锁、条件变量,都是通过"锁"来保证线程安全。但锁是有开销的——加锁解锁涉及系统调用、线程调度、上下文切换。如果临界区很小,比如只是一个计数器加一,锁的开销可能比实际操作还大。

原子操作就是来解决这个问题的:不用锁,也能保证线程安全。

什么是原子操作

原子操作就是不可分割的操作——要么完全做完,要么完全没做,不存在做到一半被其他线程看到中间状态的情况。

// 非原子操作:多线程下不安全
int counter = 0;

void increment() {
    counter++;  // 读-改-写三步,不是原子的
    // 实际执行:1.读counter 2.+1 3.写回counter
    // 两个线程同时做,可能丢失一次更新
}

// 原子操作:线程安全
atomic<int> atomicCounter{0};

void atomicIncrement() {
    atomicCounter++;  // 一条CPU指令完成,不可分割
}

atomic++在底层通常对应一条CPU指令——x86上的lock inc指令。这条指令执行期间,其他核心无法访问这个内存地址。所以不需要操作系统层面的锁,性能比mutex快得多。

atomic的基本用法

atomic<int> count{0};
atomic<bool> running{true};
atomic<double> temperature{0.0};  // C++20起支持浮点原子

// 基本操作
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);
// 如果count==10,把count改成20,返回true
// 如果count!=10,把expected更新为count的当前值,返回false

CAS(Compare-And-Swap)是无锁编程的核心操作。它的逻辑是:如果内存位置的值等于预期值,就更新为新值;否则什么都不做。整个过程是原子的。

很多无锁数据结构都是基于CAS实现的。比如无锁栈的push操作:

template<typename T>
class LockFreeStack {
    struct Node {
        T data;
        Node* next;
    };
    atomic<Node*> 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 {
    atomic<bool> emergencyStop_{false};
    atomic<int> 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就够了——帧计数不需要严格的可见性保证,大概准确就行。

顺便提一句,C++20引入了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篇 多线程之线程池设计——机器人系统中的标配组件

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐