互斥锁、自旋锁和原子操作
互斥锁和自旋锁
用法上是相似的,具有数据独占的效果;
但是自旋,代表CPU空转,不会直接进入阻塞状态,在linux C++中采用退避策略多次循环空转后未能获取锁后,最后可能移交控制权,线程进入阻塞(上下文线程切换),场景:临界区小,锁内容小。
互斥锁,该线程未获得锁时,直接进入阻塞,等到获得锁时再次唤醒,场景:临界区大,锁内容大(红黑树添加,涉及大量调整)
读写锁 shared_lock c++17:对于多个线程的读写共享内存任务,互斥锁就很臃肿,尤其时多个线程读时,因此,出现这个锁。上述情景中,写具有独占性,而读应该是共享的,读时不能写,反之同理;此外,需要一个机制,读时若有线程需要写,进入等待,则也禁止其他读线程,放置读过程的独占资源(写饿死)。
原子操作是一种在多线程中,不会被线程调度、信号中断和上下文切换所打断的轻量级无锁操作,只能查看原子操作前后的状态而没有中间态,避免了数据竞争;针对原子变量的操作;
理解原子性:一个原子操作是针对原子变量的读改写,它可以被编译为多条指令,多个指令在CPU执行时是被绑定在一起的,让其他线程看到中间结果。 一个原子只能查看执行代码前后的状态而不会让其他核心看到中间态。
存储体系结构: 如今计算机架构是多核心,多cpu架构;

从图中标识可知,访问主存大约100ns 而在一级缓存L1中只有0.9ns 本质解决cpu运算速度和内存读取速度不匹配的问题。
- 对于单核单cpu,不存在多线程并行运算,所有线程在单核上交替运行,它的原子性是指操作不会被打断,可以依靠底层自旋锁+屏蔽中断实现。
- 对于多核多cpu 实现内存原子性不仅需要保证当前指令序列不被中断打断,更关键的是要获得目标内存区域的临时独占权。现代实现优先采用“缓存锁定”机制:处理器将目标缓存行通过MESI协议的状态机置为独占(E)或修改(M)状态,并利用总线窥探(Snooping)机制实时监控总线上的内存访问请求。一旦检测到其他核心试图访问该缓存行,状态机即发送失效信号使其本地副本无效,从而将针对该内存地址的并发读写操作强制串行化(即保证读-改-写过程不可分割)。只有当目标内存跨缓存行或不可缓存时,才会退而使用传统的锁总线(LOCK#)信号,以全局阻塞所有内存访问的方式来兜底。。
- 写回策略 :
- 写: 写是否命中缓存;命中直接写,并标记为脏;没命中往下。通过LRU机制,定位缓存块后,该缓存块数据是否是脏数据,若为脏数据,脏数据刷主存,从内存读取,写入缓存,标记为脏,不是脏数据,从内存读取,写入缓存,标记为脏。
(脏数是:已经被CPU修改过,但尚未将修改结果写回到主存的数据) - 读: 是否命中缓存,命中直接读返回
没命中往下。定位缓存块后,该数据是否是脏数据
脏数据,刷主存,从内存读取,写入缓存,标记为非脏,不是脏数据,从内存读取,写入缓存,标记为非脏。
- 写: 写是否命中缓存;命中直接写,并标记为脏;没命中往下。通过LRU机制,定位缓存块后,该缓存块数据是否是脏数据,若为脏数据,脏数据刷主存,从内存读取,写入缓存,标记为脏,不是脏数据,从内存读取,写入缓存,标记为脏。
- 缓存一致性:原因是多核下由于缓存更新的数据没有重新加载到主存,其他核回读到旧值。解决方法:
- 总线嗅探bus snooping (),控制对相关内存空间的访问,等待通知,但存在通信延迟快慢的问题,补充下面的方法
- 事务串行:通过lock 和 指令“锁”,指定了接收通知的顺序,但需要尽量减少写传播中给总线带宽压力。补充下面的方法
- MESI一致性协议:基于总线窥探机制实现了事务串行化,通过状态机降低总线带宽的压力。4 个状态 Modified已修改(某个数据块已修改但是没有同步到内存中)、Exclusive独占(某个数据块只在某核心当中,此时缓存和内存中的数据一致的)、Shared共享(某个数据块在多个核心中加载,此时缓存和内存中的数据一致的)、Invalidated已失效(某个数据块在核心中已失效,不是最新的数据)。写后锁住 M\E 状态,避免相关内存的访问。
内存序:cpu和编译器会对代码进行优化使得指令执行顺序进行重排。 表现是编写的代码顺序和执行顺序的不一致问题,具体的,每个线程的语句是固定顺序;;但是线程之间的语句可能交错;
- 内存序规定了多个线程访问同一个内存地址时的语义;某个线程对内存地址的更新何时能被其它线程看见;某个线程对内存地址访问附近可以做怎么样的优化。(概括就是指导读写的先后同步和CPU编译器的优化)
- 6 种不同的内存序:memory_order_relaxed、memory_order_release(写)、memory_order_acquire(读)、memory_order_consume、memory_order_acq_rel、memory_order_seq_cst。
- 该模型可以实现内存屏障 防止代码运行越过这个屏障 可以用锁理解它的作用:
![image]()
简单说:lock(memory_order_acquire)之后的语句不能再编译优化后到其上方;而 unlock(memory_order_release)之前的语句不能。。。。后到其下方;
![image]()
![Sheet_20260805]()
常用原子操作:
C++中在
CAS机制 一种多线程中的无锁原子操作,通过比较与交换,实现乐观锁的一种方式,避免锁的开销(悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其它线程修改了)
缺点:保证了单一变量的原子性,对于涉及变量间运算的临界区是无法实现原子性的。
过程:比较内存数据是否等于预期值;是,用新值更新内存数据;否,重写预期值,回到第一步;
锁和原子操作,都避免了数据竞争并实现了数据同步。锁主要针对多个变量,而原子操作只针对单个变量。单个操作;
互斥锁的底层实现:在 Linux 上,std::mutex 是一条标准库封装 → glibc pthread_mutex → 内核 futex 的三层链路,核心思想是:无竞争时完全在用户态用原子指令解决,只有真的抢不到锁才陷入内核 。
- 层次结构大致如下:
C++ 代码
↓ std::mutex::lock()
glibc: pthread_mutex_lock()
├─ Fast Path: 原子 CAS(用户态,无系统调用) ← 大多数情况走这里
└─ Slow Path: futex(FUTEX_WAIT) → 内核态 ← 发生竞争才走这里
↓
Linux 内核: futex_wait() → 等待队列 → schedule() → 上下文切换
-
Fast Path:用户态原子抢锁,线程调用 lock() 时,第一步是用一条原子 CAS 指令(cmpxchg)尝试把锁状态从 0(未锁)改成 1(已锁):如果成功进入临界区;否则进入Slow Path;;
Slow Path 先短自旋(自适应互斥锁的机制):在用户态循环检查锁状态若干次,赌持锁线程马上释放,避免立刻上下文切换; 再标记"有等待者",把锁状态改成2(CONTENDED,已锁且有等待者),告诉未来的解锁者"释放时要唤醒别人"。最后,futex(FUTEX_WAIT) 系统调用:陷入内核,内核把当前线程挂到该futex地址对应的等待队列上,线程进入睡眠,让出 CPU。(futex中把"检查值 + 睡眠"合并成一个原子的内核操作,防止检查完到睡眠之间可能恰好有人释放了) -
unlock:用户态释放 + 必要时唤醒:只有当确实有线程在等时,解锁才会陷入内核。无竞争时 unlock 同样是零系统调用的纯用户态操作 。
持锁线程调用 unlock():
1. 原子地把 state 从 1 改回 0
2. 检查 FUTEX_WAITERS 位是否被设置
├─ 没设置:直接返回,无系统调用 ← 快路径
└─ 设置了:futex(FUTEX_WAKE, 1) 唤醒一个等待线程
- 内核侧:内核维护一张哈希表,key 是 futex word 的用户空间地址,value 是挂在该地址上的等待线程链表。 FUTEX_WAIT:读 *uaddr,若等于期望值则在哈希桶里建一个包含线程描述符的futex_q 节点,把当前线程设为 TASK_INTERRUPTIBLE 并 schedule() 出去。(schedule内核调度器(scheduler)将当前线程从运行态切换为睡眠态,并选择另一个可运行的线程来占用 CPU。)这里再次应用CAS机制如下:
int observed = atomic_load(&lock); // 读取当前锁状态
futex(&lock, FUTEX_WAIT, observed, NULL);
- FUTEX_WAKE:在哈希桶里找到等在这个地址上的线程,唤醒指定数量(通常 1 个)。
被唤醒的线程从 futex 系统调用返回用户态,再次 CAS 抢锁,成功则进入临界区。 - 顺带一提, 自旋锁的底层实现只是在用户态进行封装,通过原子操作对锁状态进行控制,使其进入忙等待,持续尝试获取锁;解锁时清除标志位为0;因此,从内核的角度,自旋锁不陷入内核,互斥锁会在内核休眠;自旋锁不会上下文切换,而互斥锁会。
下面是使用自旋锁的一个在多线程中共享一个数据的demo
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define THREAD_COUNT 10
#define ITERATIONS 10000
// static pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_spinlock_t count_spin;
void *thread_callback(void *arg) {
int *pcount = (int *)arg;
int i = 0;
while (i++ < ITERATIONS) {
// pthread_mutex_lock(&count_mutex); //这里是互斥
pthread_spin_lock(&count_spin);
(*pcount)++;
usleep(100); // 模拟一些工作负载
pthread_spin_unlock(&count_spin);
// pthread_mutex_unlock(&count_mutex);
}
return NULL;
}
int main(void) {
pthread_t threadid[THREAD_COUNT];
int count = 0;
if (pthread_spin_init(&count_spin, PTHREAD_PROCESS_PRIVATE) != 0) {
perror("pthread_spin_init");
return EXIT_FAILURE;
}
for (int i = 0; i < THREAD_COUNT; i++) {
if (pthread_create(&threadid[i], NULL, thread_callback, &count) != 0) {
perror("pthread_create");
return EXIT_FAILURE;
}
}
// for (int i = 0; i < THREAD_COUNT; i++) {
// if (pthread_join(threadid[i], NULL) != 0) {
// perror("pthread_join");
// return EXIT_FAILURE;
// }
// }
for (int i = 0; i < 100; i++) {
printf("count = %d\n", count);
sleep(1);
}
pthread_spin_destroy(&count_spin);
// printf("Final count = %d\n", count);
return EXIT_SUCCESS;
}
由于linux下提供了cas操作的接口__sync_bool_compare_and_swap:替换上面的自旋锁如下:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define THREAD_COUNT 10
#define ITERATIONS 100000
static int cas(int *addr, int expected, int desired) {
return __sync_bool_compare_and_swap(addr, expected, desired);
}
static void atomic_increment(int *value) {
int old_value;
do {
old_value = *value;
} while (!cas(value, old_value, old_value + 1));
}
void *thread_callback(void *arg) {
int *pcount = (int *)arg;
for (int i = 0; i < ITERATIONS; ++i) {
atomic_increment(pcount);
}
return NULL;
}
int main(void) {
pthread_t threadid[THREAD_COUNT];
int count = 0;
for (int i = 0; i < THREAD_COUNT; ++i) {
if (pthread_create(&threadid[i], NULL, thread_callback, &count) != 0) {
perror("pthread_create");
return EXIT_FAILURE;
}
}
for (int i = 0; i < THREAD_COUNT; ++i) {
if (pthread_join(threadid[i], NULL) != 0) {
perror("pthread_join");
return EXIT_FAILURE;
}
}
printf("Final count = %d\n", count);
return EXIT_SUCCESS;
}
手撕shared_ptr
列出实现的接口:
a. 构造函数
b. 析构函数
c. 拷贝构造函数
d. 拷贝赋值运算符
e. 移动构造函数
f. 移动赋值运算符
g. 解引用、箭头运算符、重置指针
h. 引用计数、原始指针、大小指针
要求:
不考虑删除器和空间配置器;不考虑弱引用;考虑引用计数的线程安全;提出测试案例;
数据成员细节:
空的 shared_ptr 大小为 16 字节;std::atomicstd::size_t 引用计数
接口细节:1、有参构造函数需要explict 2、拷贝构造和拷贝赋值运算符 需要参数const shared_ptr
内存序细节: 1、fetch_add 采用 memory_order_relaxed; (在引用技术减少到0之前的所有才做和其之后的操作之间不存在竞态)2、fetch_sub采用memory_order_acq_rel; 3、load采用memory_order_acquire;




浙公网安备 33010602011771号