锁和原子操作的实现

操作的原子性

10个线程同时对count变量进行++,每个线程进行10万次++,理想情况的结果是100万,但最后的结果却是小于100万的。

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

#define THREAD_COUNT   10


void *func(void *arg) {
    volatile int *pcount = (int *)arg;
    int i = 0;

    for (i = 0; i < 100000; i++) {
        (*pcount)++;
        usleep(1);
    }
}


int main() {

    pthread_t tid[THREAD_COUNT] = {0};

    int count = 0;

    int i = 0;
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_create(&tid[i], NULL, func, &count);
    }

    for (i = 0; i < 100; i++) {
        printf("count --> %d\n", count);
        sleep(1);
    }

    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tid[i], NULL);
    }

}

image.png

 结果为什么小于100万呢?因为i++并不是原子操作。

#include <stdio.h>

int i = 0;
// gcc -S 1_test_i++.c
int main(int argc, char **argv)
{
    i++;
    return 0;
}

i++ 不是原子操作

i++ 汇编代码

	movl	i(%rip), %eax //把i从内存加载到寄存器
	addl	$1, %eax //把寄存器的值加1
	movl	%eax, i(%rip) //把寄存器的值写回内存

多个线程同时进行count++,大部分时间是线程1的三条指令执行完,线程2执行

image.png

 但有时候会是线程1的第一条或者前两条指令执行,再试线程2执行,最后又是线程1的第三条指令执行, 这样就会造成最后的结果小于100万。

image.png

 这样就会造成线程不安全,要解决这个问题,就需要加锁或者使用原子操作。

互斥锁 mutex

如果获取不到锁,让出CPU,将线程加入等待队列。

任务耗时比上下文切换要长,可以使用mutex。



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

#define THREAD_COUNT   10

pthread_mutex_t mutex;


void *func(void *arg) {
    int *pcount = (int *)arg;
    int i = 0;

    for (i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        (*pcount)++;
        pthread_mutex_unlock(&mutex);
        usleep(1);
    }
}


int main() {

    pthread_t tid[THREAD_COUNT] = {0};

    int count = 0;

    pthread_mutex_init(&mutex, NULL);

    int i = 0;
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_create(&tid[i], NULL, func, &count);
    }

    for (i = 0; i < 100; i++) {
        printf("count --> %d\n", count);
        sleep(1);
    }

    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tid[i], NULL);
    }

}

image.png

 自旋锁spinlock

如果获取不到锁,则继续死循环检查锁的状态,如果是lock状态,则继续死循环,否则上锁,结束死循环。

(1)任务不能存在阻塞 (2)任务耗时短,几条指令

PTHREAD_PROCESS_SHARED, 表示fork出来的进程可以共用。

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

#define THREAD_COUNT   10

pthread_spinlock_t spinlock;


void *func(void *arg) {
    int *pcount = (int *)arg;
    int i = 0;

    for (i = 0; i < 100000; i++) {
        pthread_spin_lock(&spinlock);
        (*pcount)++;
        pthread_spin_unlock(&spinlock);
        usleep(1);
    }
}


int main() {

    pthread_t tid[THREAD_COUNT] = {0};

    int count = 0;

    pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);

    int i = 0;
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_create(&tid[i], NULL, func, &count);
    }

    for (i = 0; i < 100; i++) {
        printf("count --> %d\n", count);
        sleep(1);
    }

    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tid[i], NULL);
    }

}

image.png

 mutex和spinlock的使用场景比较

  1. 临界资源操作简单/没有系统调用,选择spinlock
  2. 操作复杂/有系统调用,选择mutex。

主要是看操作是比线程切换简单还是复杂。

读写锁

适用于读多写少的场景,一般不推荐使用读写锁

原子操作

使用汇编指令实现++i, 通过CPU指令将++操作在一条指令内实现。



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

#define THREAD_COUNT   10

int inc(int *value, int add) {
    int old;
/*
xaddl
交换第一个操作数(目标操作数)与第二个操作数(源操作数),然后将这两个值的和加载到目标操作数。目标操作数可以是寄存器或内存位置;源操作数是寄存器。

此指令可以配合 LOCK 前缀使用。

TEMP  SRC + DEST
SRC  DEST
DEST  TEMP
*/
    __asm__ volatile (
        "lock; xaddl %2, %1;" // lock表示锁住CPU操作内存的总线,%2代表add %1代表*value
        : "=a" (old) // output old=eax
        : "m" (*value), "a" (add) // input m是原始内存,将add值放入eax
        : "cc", "memory"
    );
    return old;
}


void *func(void *arg) {
    int *pcount = (int *)arg;
    int i = 0;

    for (i = 0; i < 100000; i++) {
        inc(pcount, 1);
        usleep(1);
    }
}


int main() {

    pthread_t tid[THREAD_COUNT] = {0};

    int count = 0;

    int i = 0;
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_create(&tid[i], NULL, func, &count);
    }

    for (i = 0; i < 100; i++) {
        printf("count --> %d\n", count);
        sleep(1);
    }

    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_join(tid[i], NULL);
    }

}

image.png

lock锁的是CPU操作内存的总线

原子操作需要CPU指令集支持才行。

CAS(Compare and Swap)

CAS比较并交换,是原子操作的一种,先对比再赋值

Compare And Swap
if (a == b) {
    a = c;
}

cmpxchg(a, b, c)

bool CAS( int * pAddr, int nExpected, int nNew )
atomically {
    if ( *pAddr == nExpected ) {
        *pAddr = nNew ;
        return true ;
    }
    return false ;
}

具体汇编代码实现,可以参考zmq无锁队列中cas实现。

image.png

原子操作实现的方式

  1. gcc、g++编译器提供了一组原子操作api;
  2. C++11也提供了一组原子操作api;
  3. 也可以用汇编来实现

总结

对于复杂的情况,包括读文件、socket操作,系统调用,可以使用mutex;

操作比较简单,原子操作不支持的,可以使用spinlock;

如果CPU指令集支持,可以使用原子操作。

线程切换,消耗资源,代价比较大,体现在哪里?

如果只是mov寄存器,其实代价是很小的;线程切换,除了寄存器操作,还需要进行文件系统,虚拟内存的切换,所以代价比较大。

mutex, mutex_trylock, spinlock 锁的粒度依次减小。

mutex适合锁执行之间比较长的一段代码,粒度最大,比如锁整颗B+数或者红黑树。获取不到锁的话,会进行线程的切换;

mutex_trylock,尝试获取锁,立刻就返回,本身不进行线程的切换,在应用程序中通常会用一个while循环来驱动,但是因为是在应用层,有可能会有线程切换。

spinlock,是在内核中不断尝试获取锁,不进行线程切换,锁的粒度最小。锁住CPU, 底层核心代码实现是一个loop指令,不断检查条件是否满足,条件满足才返回。

锁粒度的大小怎么判断?

如果要锁的内容的执行时间,要比线程切换的代价大,就算是大粒度,可以使用mutex;如果锁的内容的执行时间很短,比如就几行代码,加入个队列这种,就算是小粒度,可以使用spinlock。

如果会发生死锁,无论使用mutex还是spinlock都不可以。

原子操作,粒度更小,需要看是否支持单条汇编指令。

rwlock,读写锁,适合读多写少的场景。​​​​​​​

<think>嗯,用户想了解如何使用原子操作实现互斥。这个问题涉及并发编程的核心机制,需要仔细解释清楚原理实现步骤。 用户可能正在学习操作系统或并发编程,需要理解底层同步原语的实现方式。从问题表述看,用户应该已经了解基本概念,现在想深入实现层面。这类知识对开发高性能并发系统很重要。 实现互斥的关键在于原子操作,特别是CAS(Compare-And-Swap)指令。CAS允许我们原子地检查并修改内存值,这是构建的基础。让我想想如何清晰地解释这个机制。 首先需要理解互斥的基本要求:同一时刻只能有一个线程进入临界区。这需要两个核心状态:定状态等待队列。但原子操作实现的简易版本通常只处理定状态,等待功能需要其他机制配合。 参考引用[3]中提到的自旋实现是个很好的起点:通过循环尝试CAS操作直到成功获取。不过这个实现会导致忙等待(busy-waiting),在竞争激烈时浪费CPU资源。 完整的实现应该包括以下部分: 1. 定义结构体,包含原子标志位 2. 加函数:循环尝试将标志从0改为1 3. 解函数:简单将标志重置为0 4. 考虑内存顺序问题,避免指令重排导致错误 用户可能还会关心性能优化。比如在加失败时让出CPU(使用sched_yield),或者使用更高级的队列算法(如MCS)。不过这些属于进阶内容,初次实现可以简化。 需要特别注意ABA问题:虽然CAS能保证原子性,但如果标志位在检查期间经历了0->1->0的变化,简单的CAS实现会错误地获取。不过对于互斥标志位,这个场景通常不会造成问题,因为被释放后其他线程可以合法获取。 最后要提醒用户,这种自旋适用于临界区很短的情况。对于长时间持有的,应该考虑使用系统提供的互斥,它们通常会在等待时挂起线程,避免CPU浪费。</think>### 使用原子操作实现互斥的方法 互斥的核心目标是**确保临界区的独占访问**。通过原子操作(如 **CAS**)可实现简易的自旋(Spinlock),其本质是一种忙等待的互斥。以下是实现步骤代码示例: #### 核心原理 1. **原子标志位**:使用一个原子变量(如 `flag`)表示状态(`0`=未定,`1`=定)。 2. **CAS 操作**:加时循环尝试将 `flag` 从 `0` 原子地改为 `1`,若成功则获得;解时直接将 `flag` 重置为 `0`。 #### C++ 实现示例 ```cpp #include <atomic> class SpinlockMutex { private: std::atomic_flag flag = ATOMIC_FLAG_INIT; // 原子标志位 public: void lock() { // 循环尝试直到成功获取 while (flag.test_and_set(std::memory_order_acquire)) { // 可选优化:减少CPU占用(如调用线程暂停指令) #ifdef __x86_64__ __builtin_ia32_pause(); // x86架构的PAUSE指令 #endif } } void unlock() { flag.clear(std::memory_order_release); // 释放 } }; ``` #### 关键机制详解 1. **加过程**: - `test_and_set()` 是原子操作:**读取当前值并同时设置为 `true`**。 - 若返回值为 `false`(表示原状态为未定),则线程成功获取。 - 若返回值为 `true`(表示已被占用),线程在循环中忙等待(自旋)。 2. **内存序参数**: - `memory_order_acquire`:确保**加后**的临界区操作不会被重排到加前。 - `memory_order_release`:确保**解前**的临界区操作不会被重排到解后。 - 二者共同构成 **happens-before** 关系,保证数据访问安全[^3][^4]。 3. **优化点**: - **PAUSE 指令**:在 x86 架构中插入暂停指令,减少自旋时的 CPU 功耗及总线争用。 - **指数退避**:在多次自旋失败后主动让出 CPU(如 `std::this_thread::yield()`),避免过度占用资源。 #### 适用场景与局限性 | **场景** | **局限性** | |-------------------|-------------------------------| | 临界区代码极短(如计数器) | 长时间自旋浪费 CPU 资源 | | 内核/无阻塞系统编程 | 线程优先级反转风险 | | 低竞争环境 | 高竞争时性能急剧下降[^1][^4] | > ⚠️ **注意**: > 1. 自旋在用户空间编程中应谨慎使用,系统级互斥(如 `std::mutex`)通常更高效且公平。 > 2. 复杂场景(如嵌套、条件变量)需结合队列调度机制(如 MCS )[^3]。 --- ### 相关问题 1. **自旋与互斥的性能差异主要体现在哪些场景?** 2. **如何用原子操作实现支持超时功能的互斥?** 3. **为什么高竞争环境下自旋可能降低整体系统吞吐量?** 4. **CAS 操作在哪些情况下会出现 ABA 问题?对实现有何影响?** [^1]: 原子操作在频繁变更场景下的劣势(乐观假设易失败)。 [^3]: 基于 CAS 的自旋基础实现原理。 [^4]: 原子操作与互斥的适用场景对比(简单状态 vs 复杂临界区)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值