使用内联汇编实现CAS操作(含详细讲解)

在多线程环境下,如何安全地更新共享变量,一直是一个重要的话题。今天,我们通过一段使用内联汇编实现的CAS(Compare And Swap)代码,深入学习它的原理和用法。

完整示例代码如下:

#include       // 标准输入输出头文件
#include     // pthread多线程编程相关头文件
#include      // usleep函数需要的头文件

#define THREAD_COUNT 10 // 定义线程数量为10

volatile int count = 0; // 全局变量,volatile保证每次都从内存读取,防止编译器优化导致数据过时

// 使用内联汇编实现CAS(Compare And Swap)函数
int cas(volatile int *addr, int expected, int newval) {
    unsigned char ret;
    __asm__ volatile(
        "lock; cmpxchgl %2, %1\n\t" // lock前缀保证多核下的原子性,cmpxchgl比较并交换
        "sete %0\n\t"               // 根据比较结果设置ret,如果成功置为1,否则为0
        : "=q"(ret), "+m"(*addr)     // 输出部分:ret是结果,*addr是读写的内存地址
        : "r"(newval), "a"(expected) // 输入部分:newval是要写的新值,expected放在EAX寄存器
        : "memory", "cc"             // 告诉编译器这段代码会修改内存和标志位
    );
    return ret; // 返回CAS是否成功
}

// 线程执行的回调函数
void *thread_callback(void *arg) {
    int i = 0;

    while (i++ < 10000) {  // 每个线程自增10000次
        int old;
        do {
            old = count;                // 先读取当前count值
        } while (!cas(&count, old, old + 1)); // 尝试将count加1,失败则重试

        usleep(1); // 睡眠1微秒,适当让出CPU,避免长时间占用
    }

    return NULL;
}

int main() {
    pthread_t threads[THREAD_COUNT]; // 定义线程ID数组

    // 创建多个线程
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_create(&threads[i], NULL, thread_callback, NULL);
    }

    // 等待所有线程执行完毕
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(threads[i], NULL);
    }

    // 所有线程结束后,打印最终的count值
    printf("最终count值:%d\n", count);

    return 0;
}

一、什么是CAS?

CAS(Compare And Swap,比较并交换)是一种无锁的同步机制。

它的基本思想是

先比较内存中的值是否是自己期望的,如果是,就交换成新值;否则,不做任何操作,并通知调用者。

这样可以保证只有看到的值没被别人改过时,自己的修改才生效,否则就要重新尝试。
这是一种非常高效的并发控制手段,常见于操作系统、数据库、JVM等系统中。

二、pthread_create 和 pthread_join 的作用

main 函数中,我们通过 pthread_create 创建了10个子线程,每个线程都执行 thread_callback 函数,进行1万次自增操作。

pthread_join 的作用是等待对应线程执行完毕。如果没有 pthread_join,主线程可能在子线程还没执行完时就退出,导致程序提前结束。所以每次开启线程后,最后一定要用 pthread_join 把它们一个个“回收”回来,确保所有子线程跑完再输出最终结果。

三、volatile 在这里的作用

volatile 关键字告诉编译器:不要优化访问这个变量,每次都要直接从内存读取它的最新值。
这是因为在多线程环境下,count 可能随时被其他线程修改,如果编译器为了优化性能缓存了 count 的值,就可能读到过期的数据,导致CAS判断失效。

所以,volatile 在这里是非常必要的,保证读取的是最新、真实的内存数据。

四、内联汇编实现CAS的细节

我们手动实现的 cas 函数用到了 GCC 的内联汇编:

__asm__ volatile(
    "lock; cmpxchgl %2, %1\n\t"
    "sete %0\n\t"
    : "=q"(ret), "+m"(*addr)
    : "r"(newval), "a"(expected)
    : "memory", "cc"
);

简单解释一下每一部分:

  • lock; cmpxchgl %2, %1
    lock 前缀的 cmpxchgl 指令,保证在多核CPU上也是原子性的
    逻辑是:如果 EAX(也就是 expected)等于内存中的值 *addr,就把 newval 写进去,否则什么都不做,并把当前内存值加载到 EAX

  • sete %0
    根据上一步比较结果,设置 ret。如果比较成功(内存中原本的值和expected一样),ret=1,否则ret=0

  • "=q"(ret):把成功与否的结果输出到ret。

  • "+m"(*addr):表示 *addr 是一个会被读写的内存操作数。

  • "r"(newval), "a"(expected):输入 newval 和 expected,注意 expected 必须放在 EAX 中,因为 cmpxchg 指令要求。

  • "memory", "cc":告诉编译器,这段汇编会修改内存(memory)和标志寄存器(cc)。

五、为什么 old+1 之后,old 还是可能和 count 不一致? 

在多线程环境下,
old = count; 这一步只是读取了某一时刻count,但在执行 cas(&count, old, old + 1) 之前,其他线程可能已经修改了 count

所以两种情况:

  • 如果没有其他线程修改 count
    那么 old == count,CAS成功,把 count 修改为 old+1

  • 如果有其他线程修改了 count
    那么 old != count,CAS失败,循环重新读取新的 count,继续尝试。

举个例子更直观:

  1. 线程A读取到 count = 5old = 5,准备把它改成6。

  2. 线程B也读取到 count = 5old = 5,也准备把它改成6。

  3. 线程A先执行CAS,成功,把 count改成了6。

  4. 线程B随后执行CAS,发现内存中的 count 已经变成6了,跟自己记住的 old=5 不一样,所以CAS失败,B重新读取新的count,再尝试。

总结一句话

old只是你当时看到的 count,真实世界中,count 可能早就变了。
只有当 old 和当前 count 一致时,CAS才会成功,否则就要重新来一次。

 这就是CAS操作的核心思想:
看到的是最新的才允许修改如果期间被别人改了就放弃,重新来过

小结

本文完整实现了一个基于CAS的多线程安全自增,讲解了 pthread 线程管理、volatile 保证可见性,以及 CAS 原理和底层内联汇编的使用细节。
通过这个例子,可以更深入理解为什么CAS是高性能并发编程中不可或缺的基础技术。

https://github.com/0voice

你可能感兴趣的:(汇编)