多线程同步:互斥锁、自旋锁与原子操作讲解(附代码示例)

在多线程编程中,为了保证对共享资源的访问不会发生竞争条件(race condition),通常需要引入同步机制。本篇将结合一个实际的例子,讲解互斥锁(Mutex)、自旋锁(Spinlock)原子操作(Atomic Operation)的原理与使用场景。

示例代码

下面是一段简单的多线程程序,多个线程对一个共享变量 count 进行累加操作。为了演示不同的同步方式,这段代码预留了四种写法,通过修改 #if 0 的宏开关,可以切换不同的同步方法。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

#define THREAD_COUNT 10

pthread_mutex_t mutex;
pthread_spinlock_t spinlock;

// 使用x86汇编实现的原子加法
int inc(int *value, int add) {
    int old;
    __asm__ volatile(
        "lock; xaddl %2,%1;"
        : "=a" (old)
        : "m" (*value), "a" (add)
        : "cc", "memory"
    );
    return old;
}

void *thread_callback(void *arg) {
    int *pcount = (int *)arg;
    int i = 0;
    while (i++ < 100000) {
#if 0
        (*pcount)++; // 普通加法(无保护)
#elif 0
        pthread_mutex_lock(&mutex);
        (*pcount)++;
        pthread_mutex_unlock(&mutex);
#elif 0
        inc(pcount, 1); // 原子加法
        pthread_spin_lock(&spinlock);
        (*pcount)++;
        pthread_spin_unlock(&spinlock);
#else
        inc(pcount, 1); // 纯原子加法
#endif
        usleep(1); // 稍作休眠,模拟真实环境
    }
    return NULL;
}

int main() {
    pthread_t threadid[THREAD_COUNT] = {0};
    pthread_mutex_init(&mutex, NULL);
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
    
    int i = 0;
    int count = 0;
    
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_create(&threadid[i], NULL, thread_callback, &count);
    }
    
    for (i = 0; i < 100; i++) {
        printf("count : %d\n", count);
        sleep(1);
    }
    return 0;
}

1. 互斥锁(Mutex)

互斥锁用于保护共享资源不被多个线程同时访问,从而避免数据竞争和不一致性问题。

优点

  • 适合临界区较长的场景,避免CPU空转浪费资源。

  • 简单易用。

缺点

  • 有上下文切换开销:当线程挂起和唤醒时,操作系统需要进行线程调度,代价比较大。

代码示例:

pthread_mutex_lock(&mutex);
(*pcount)++;
pthread_mutex_unlock(&mutex);

2. 自旋锁(Spinlock)

原理

当一个线程尝试获取已被占用的自旋锁时,它会一直循环检查锁是否可用(即“自旋”),而不会让出CPU。

优点

  • 适合临界区非常短的场景(加锁时间短、线程很快就释放锁)。

  • 避免了线程挂起、唤醒带来的系统调用开销。

缺点

  • 消耗大量CPU,如果锁等待时间长,会导致性能下降。

  • 不适合高并发且锁粒度粗大的场景。

代码示例:

pthread_spin_lock(&spinlock);
(*pcount)++;
pthread_spin_unlock(&spinlock);

3. 原子操作(Atomic Operation)

原理

原子操作是通过硬件指令(比如x86的lock前缀指令)保证某个操作不可中断,一旦开始执行就一直执行完毕。

__asm__ volatile(
    "lock; xaddl %2,%1;"
    : "=a" (old)
    : "m" (*value), "a" (add)
    : "cc", "memory"
);
  • lock 保证了总线锁定,防止其他CPU访问同一内存地址。

  • xaddl交换并加指令。

优点

  • 无需显式加锁,速度极快

  • 避免了锁竞争和上下文切换。

缺点

  • 仅适合简单的读-改-写操作,复杂逻辑还是需要锁保护。

  • 编写和理解原子指令较为复杂。

4. 不同方式的对比总结

方式适用场景优点缺点
普通操作单线程环境简单,快速多线程下出现竞争条件
互斥锁多线程+临界区长简单,保护性强可能引发线程挂起,调度开销大
自旋锁多线程+临界区短快速避免阻塞高CPU占用,易造成性能问题
原子操作极短操作(如计数器)极高性能功能有限,复杂逻辑不适用

额外知识补充:关于 #if 0 这些宏指令的小技巧

#if 0
    (*pcount)++;
#elif 0
    pthread_mutex_lock(&mutex);
    (*pcount)++;
    pthread_mutex_unlock(&mutex);
#elif 0
    inc(pcount,1);
    pthread_spin_lock(&spinlock);
    (*pcount)++;
    pthread_spin_unlock(&spinlock);
#else
    inc(pcount,1);
#endif

这一块使用了预处理指令,特别是 #if#elif#else#endif

1. #if / #elif / #else / #endif 是什么?

它们是 条件编译指令,在编译阶段(还没真正编译成机器代码时),由编译器的预处理器决定哪些代码需要被保留、哪些代码需要被忽略

  • #if 0 表示这块代码不会被编译进去(因为 0 是假,条件不成立)。

  • #if 1 表示这块代码会被编译

  • #elifelse if的缩写,可以继续判断其他条件。

  • #else 表示以上都不满足时,执行这一块。

  • #endif 表示条件编译结束。

简单来说:这就像一个只在编译阶段起作用的 if-else 判断!

2. 为什么要这样写?

主要是为了方便切换测试不同的代码块
比如在多线程同步示例中,我可以很快地切换不同的写法,只要把对应的 0 改成 1

  • 想测试普通加法:把第一个 #if 0 改成 #if 1

  • 想测试加锁版本:把第二个 #elif 0 改成 #elif 1

  • 想测试原子操作:把第三个 #elif 0 改成 #elif 1

https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值