目录
-
- C++中的锁
-
- 1、什么是锁
- 2、C++中有那些锁机制
- [3、基础互斥锁(mutex 系列)](#3、基础互斥锁(mutex 系列))
- 4、读写锁(共享锁)
- 5、自旋锁(spinlock)
- 6、一次性初始化锁
- 7、原子操作(无锁)
-
- 7.1、memory_order_relaxed
- [7.2 memory_order_release](#7.2 memory_order_release)
- [7.3 memory_order_acquire](#7.3 memory_order_acquire)
- [7.4 memory_order_consume](#7.4 memory_order_consume)
- [7.5 memory_order_acq_rel](#7.5 memory_order_acq_rel)
- [7.6 memory_order_seq_cst](#7.6 memory_order_seq_cst)
- 结语
C++中的锁
1、什么是锁
锁(Lock 是一种 同步机制,用来保证 多个线程或进程在访问共享资源时不会发生冲突。
2、C++中有那些锁机制
1、基础互斥锁(mutex 系列)
2、读写锁(共享锁),由C++17引入
3、自旋锁(spinlock)
4、一次性初始化锁
5、原子操作(无锁)
3、基础互斥锁(mutex 系列)
3.1、std::mutex
3.1.1、std::mutex的lock和unlock
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <mutex>
int Count = 100;
std::mutex mtx;
void Work_up()
{
while (Count < 1000)
{
mtx.lock();
Count++;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mtx.unlock();
}
}
void Work_down()
{
while (Count > 0)
{
mtx.lock();
Count--;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mtx.unlock();
}
}
int main()
{
std::thread th1(Work_up);
std::thread th2(Work_down);
th1.join();
th2.join();
return 0;
}
1、mtx.lock():上锁 ,进入临界区。
2、mtx.unlock():解锁 ,离开临界区。
3、如果两个线程同时调用 lock(),只有一个线程能获得锁,另一个线程会阻塞等待。
4、一定要注意使用std::mutex lock后一定要手动unlock。
3.1.2、std::mutex中的std::mutex::try_lock
std::mutex::try_lock
1、mtx.try_lock() 尝试获取锁 但不会阻塞。
2、如果锁被其他线程占用,它会 立即返回 false。
3、如果成功获取锁,返回 true,线程就进入临界区。
4、使用 try_lock() 后,必须手动解锁。
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <mutex>
int Count = 10;
std::mutex mtx;
void Work_up()
{
while (Count < 100)
{
mtx.lock();
Count++;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mtx.unlock();
}
}
void Work_down()
{
while (Count > 0)
{
if (mtx.try_lock())
{
Count--;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mtx.unlock();
}
else
{
printf("%s : Count is occupy\n", __FUNCTION__);
}
}
}
int main()
{
std::thread th1(Work_up);
std::thread th2(Work_down);
th1.join();
th2.join();
return 0;
}
如果你运行这个代码会发现一个很有趣的现象,th2只有等到th1执行完成后,才能执行到**Count--**里面。
1、因为lock() 是阻塞的 → 线程必须等到锁释放才能进入临界区。
2、try_lock() 是非阻塞的 → 线程获取不到锁就直接失败。
在程序中:
Work_up 循环非常频繁,几乎持续占用锁。
Work_down 用 try_lock() 时,几乎每次尝试都失败 → Count-- 很少执行。
所以你看到的现象:th1 执行完成后,th2 才能开始真正修改 Count。
3.2、RALL概念
RAII = Resource Acquisition Is Initialization
中文意思:资源获取即初始化
核心思想:在对象创建时获取资源,在对象销毁时释放资源(通过析构函数自动释放)
| 方面 | 优点 | 缺点 / 局限 |
|---|---|---|
| 资源管理 | 自动获取和释放资源,避免忘记释放 | 生命周期严格绑定作用域,无法延长或提前释放资源 |
| 异常安全 | 即使函数异常,资源也能自动释放 | 析构顺序复杂时,异常处理逻辑可能不直观 |
| 代码简洁 | 不需要手动调用释放函数,减少错误 | 对某些需要手动精确控制的资源不方便 |
| 线程安全 | 用 RAII 管理 mutex(如 lock_guard、unique_lock)可自动解锁,减少死锁风险 |
频繁创建/销毁对象可能有微小性能开销 |
| 适用范围 | C++ 对象、智能指针、文件句柄、mutex 等 | 对纯 C 风格或全局资源封装不够直观,需要适配包装类 |
3.3、lock_guard和unique_lock
3.3.1、std::lock_guard
特点:
1、构造时加锁,析构时自动解锁
2、不能手动解锁/重新加锁 <----特别注意!
3、适合临界区短且锁不需要转移/延迟的场景
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <mutex>
int Count = 100;
std::mutex mtx;
void Work_up()
{
while (Count < 1000)
{
std::lock_guard<std::mutex> mt(mtx);
Count++;
printf("%s : Count is %d\n", __FUNCTION__, Count);
}
}
void Work_down()
{
while (Count > 0)
{
std::lock_guard<std::mutex> mt(mtx);
Count--;
printf("%s : Count is %d\n", __FUNCTION__, Count);
}
}
int main()
{
std::thread th1(Work_up);
std::thread th2(Work_down);
th1.join();
th2.join();
return 0;
}
3.3.2、std::unique_lock
3.3.2.1、unique_lock和lock_guard
unique_lock 相比较于lock_guard功能更加丰富。
| 功能 | lock_guard | unique_lock | 示例 |
|---|---|---|---|
| 自动加锁/解锁 | ✅ | ✅ | std::lock_guard<std::mutex> ul(mtx);、std::unique_lock<std::mutex> ul(mtx); |
| 延迟加锁 | ❌ | ✅ | std::unique_lock<std::mutex> ul(mtx, std::defer_lock); |
| 尝试加锁 | ❌ | ✅ | std::unique_lock<std::mutex> ul(mtx, std::try_to_lock); |
| 手动解锁/加锁 | ❌ | ✅ | ul.unlock(); ul.lock(); |
| 与条件变量 | ❌ | ✅ | cv.wait(ul, []{return ready;}); |
| 移动锁所有权 | ❌ | ✅ | std::unique_lock<std::mutex> ul2 = std::move(ul1); |
3.3.2.2、unique_lock的自动加锁/解锁
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <mutex>
int Count = 100;
std::mutex mtx;
void Work_up()
{
while (Count < 1000)
{
std::unique_lock<std::mutex> mt(mtx);
Count++;
printf("%s : Count is %d\n", __FUNCTION__, Count);
}
}
void Work_down()
{
while (Count > 0)
{
std::unique_lock<std::mutex> mt(mtx);
Count--;
printf("%s : Count is %d\n", __FUNCTION__, Count);
}
}
int main()
{
std::thread th1(Work_up);
std::thread th2(Work_down);
th1.join();
th2.join();
return 0;
}
3.3.2.3、unique_lock的延迟加锁
这个操作我感觉和直接用std::mutex手动加锁和解锁没区别。
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <mutex>
int Count = 100;
std::mutex mtx;
void Work_up()
{
std::unique_lock<std::mutex> mt(mtx, std::defer_lock);
while (Count < 1000)
{
mt.lock();
Count++;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mt.unlock();//unique_lock虽然可以自动解锁,但是是再unique_lock析构的时候。
//这里Work_up不执行完成unique_lock不会析构,所以需要手动解锁
}
}
void Work_down()
{
std::unique_lock<std::mutex> mt(mtx, std::defer_lock);
while (Count > 0)
{
mt.lock();
Count--;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mt.unlock();//同上必须手动解锁
}
}
int main()
{
std::thread th1(Work_up);
std::thread th2(Work_down);
th1.join();
th2.join();
return 0;
}
3.3.2.4、unique_lock的尝试加锁
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <mutex>
int Count = 100;
std::mutex mtx;
void Work_up()
{
std::unique_lock<std::mutex> mt(mtx, std::defer_lock);
while (Count < 1000)
{
if (mt.try_lock())
{
Count++;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mt.unlock();
}
else
{
printf("%s : Count is occupy\n", __FUNCTION__);
}
}
}
void Work_down()
{
std::unique_lock<std::mutex> mt(mtx, std::defer_lock);
while (Count > 0)
{
if (mt.try_lock())
{
Count--;
printf("%s : Count is %d\n", __FUNCTION__, Count);
mt.unlock();
}
else
{
printf("%s : Count is occupy\n", __FUNCTION__);
}
}
}
int main()
{
std::thread th1(Work_up);
std::thread th2(Work_down);
th1.join();
th2.join();
return 0;
}
这个例子和上面std::mutex 的try_lock现象一样,th2只有等到th1执行完成后,才能执行到**Count--**里面,原因也同上。
3.3.2.5、unique_lock构造时尝试加锁
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <mutex>
int Count = 100;
std::mutex mtx;
void Work_up()
{
std::unique_lock<std::mutex> mt(mtx, std::try_to_lock); // 非阻塞
while (Count < 1000)
{
if (mt.owns_lock()) // 判断加锁是否成功
{
Count++;
printf("%s owns_lock : Count is %d\n", __FUNCTION__, Count);
}
else
{
mt.lock(); // 阻塞,也可使用mt.try_lock()非阻塞式
Count++;
printf("%s lock : Count is %d\n", __FUNCTION__, Count);
}
mt.unlock(); // 必须手动解锁
}
}
void Work_down()
{
std::unique_lock<std::mutex> mt(mtx, std::try_to_lock); // 非阻塞
while (Count > 0)
{
if (mt.owns_lock()) // 判断加锁是否成功
{
Count--;
printf("%s owns_lock : Count is %d\n", __FUNCTION__, Count);
}
else
{
mt.lock(); // 阻塞,也可使用mt.try_lock()非阻塞式
Count--;
printf("%s lock : Count is %d\n", __FUNCTION__, Count);
}
mt.unlock(); // 必须手动解锁
}
}
int main()
{
std::thread th1(Work_up);
std::thread th2(Work_down);
th1.join();
th2.join();
return 0;
}
总结
css
┌─────────────────────────────┐
│ std::unique_lock │
│ 构造方式/方法 │
└─────────────────────────────┘
│
┌──────────────┼───────────────┐
│ │
defer_lock try_to_lock
(延迟加锁) (构造时尝试加锁)
│ │
│ │
lock()/unlock() owns_lock()
手动上锁/解锁 判断是否成功
│ │
│ │
│ │
▼ ▼
---------------- ----------------
构造时不加锁 构造时尝试加锁
必须后续调用 lock() 成功 → owns_lock()==true
try_lock() 可用 失败 → owns_lock()==false
可后续调用 lock()/try_lock()
3.3.2.6、unique_lock和条件变量
一个简单的生产消费模型
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
int i = 0;
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
void producer()
{
while (1)
{
{
std::unique_lock<std::mutex> lock(mtx);
i++;
q.push(i);
std::cout << "Produced: " << i << std::endl;
cv.notify_one(); // 通知消费者
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer()
{
while (1)
{
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return (q.size() > 10); }); // wait 会自动释放锁s
int value = q.front();
q.pop();
std::cout << "Consumed: " << value << std::endl;
}
}
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
为什么unique_lock 支持条件变量lock_guard 不支持?
因为条件变量中的cv.wait中会去手动解锁和加锁,unique_lock 支持手动解锁,lock_guard不支持手动解锁。
3.3.2.7、unique_lock的移动锁所有权
cpp
#include <iostream>
#include <map>
#include <mutex>
#include <thread>
#include <string>
class SafeCache
{
private:
std::map<std::string, int> cache;
std::mutex mtx;
public:
// 返回 unique_lock,调用者控制锁的生命周期
std::unique_lock<std::mutex> lockCache()
{
std::unique_lock<std::mutex> lock(mtx);
printf("Cache locked inside function\n");
// return std::move(lock); // 移动给调用者,但是最好不要这样写
return lock;//NRVO(局部命名变量直接在调用者空间构造,避免拷贝/移动)更高效
}
void insert(const std::string &key, int value)
{
auto lock = lockCache(); // 获取锁
cache[key] = value;
printf("Inserted [%s] = %d\n", key.c_str(), cache[key]);
// 离开作用域自动解锁
}
void processWithLock()
{
auto lock = lockCache(); // 锁在外部可控
// 临界区:可以安全处理缓存数据
for (auto &iter : cache)
{
iter.second = iter.second + 1;
}
printf("Processed cache safely with lock\n");
// 离开作用域时 lock 自动释放
}
std::map<std::string, int> GetData()
{
return cache;
}
};
int main()
{
SafeCache cache;
// 多线程操作缓存
std::thread t1([&](){ cache.insert("apple", 10); });
std::thread t2([&](){ cache.processWithLock(); });
t1.join();
t2.join();
for (auto iter : cache.GetData())
{
printf("k : %s, v : %d\n", iter.first.c_str(), iter.second);
}
return 0;
}
查看上述例子,可能会有疑问,这样写不是也可以吗。
cpp
void insert(const std::string &key, int value)
{
std::unique_lock<std::mutex> lock(mtx);
cache[key] = value;
printf("Inserted [%s] = %d\n", key.c_str(), cache[key]);
// 离开作用域自动解锁
}
void processWithLock()
{
std::unique_lock<std::mutex> lock(mtx);
// 临界区:可以安全处理缓存数据
for (auto &iter : cache)
{
iter.second = iter.second + 1;
}
printf("Processed cache safely with lock\n");
// 离开作用域时 lock 自动释放
}
| 特性 | 不返回写再函数内部 | 返回 unique_lock 的写法 |
|---|---|---|
| 锁的创建 | 函数内部创建,作用域结束自动解锁 | 函数内部创建,通过返回移动给调用者,调用者决定解锁时机 |
| 锁的控制 | 完全由函数控制 | 调用者可以控制锁的生命周期 |
| 灵活性 | 函数执行完就释放锁 | 可以在调用者作用域里做更多操作,保证临界区连续性 |
| 使用场景 | 简单函数内部操作 | 需要延长锁的持有时间或跨多个操作保持锁 |
3.4、std::mutex、std::lock_guard和std::unique_lock总结
| 对象类型 | 可否全局 | 用途 | 线程安全性 | 生命周期安全 |
|---|---|---|---|---|
std::mutex |
✅ 可以 | 真正的锁 | ✅ 线程安全 | ✅ 安全 |
std::unique_lock |
❌ 不推荐 | 管理锁生命周期 | ❌ 线程不安全 | ✅ 局部安全 |
std::lock_guard |
❌ 不推荐 | 简化的 RAII 锁管理 | ❌ 线程不安全 | ✅ 局部安全 |
4、读写锁(共享锁)
C++ 中的 读写锁(Read-Write Lock,也叫共享互斥锁 Shared Mutex) 用于 允许多个线程同时读共享资源,但写操作是互斥的。这是对普通 std::mutex 的一种增强,可以提高读多写少场景的并发性能。
| 操作类型 | 允许同时访问? | 阻塞条件 |
|---|---|---|
| 读(共享) | 多个线程可以同时读 | 如果有写线程正在持有锁,读线程会阻塞 |
| 写(独占) | 仅允许一个线程写 | 如果有读线程或写线程正在持有锁,写线程会阻塞 |
cpp
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <unordered_map>
#include <string>
#include <chrono>
#include <vector>
#include <mutex>
class ConfigTable
{
public:
void setConfig(const std::string &key, const std::string &value)
{
std::unique_lock<std::shared_mutex> lock(mutex_); // 写锁
configs_[key] = value;
std::cout << "[Writer] Set " << key << " = " << value << "\n";
}
std::string getConfig(const std::string &key)
{
std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁
auto it = configs_.find(key);
if (it != configs_.end())
return it->second;
return "N/A";
}
private:
std::unordered_map<std::string, std::string> configs_;
mutable std::shared_mutex mutex_; // 读写锁
};
// 全局配置对象
ConfigTable gConfig;
void writerThread()
{
int version = 1;
while (version <= 5)
{
gConfig.setConfig("version", "v" + std::to_string(version));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
++version;
}
}
void readerThread(int id)
{
for (int i = 0; i < 10; ++i)
{
std::string v = gConfig.getConfig("version");
std::cout << "Reader " << id << " reads version = " << v << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
std::vector<std::thread> threads;
threads.emplace_back(writerThread);
for (int i = 0; i < 3; ++i)
threads.emplace_back(readerThread, i);
for (auto &t : threads)
t.join();
return 0;
}
简单点说就是。允许多个线程同时读,但是只允许一个线程写,写的时候读线程阻塞,但需要注意shared_mutex是C++17才支持的。
C++11实现shared_mutexdemo:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <chrono>
class RWLock
{
private:
std::mutex mtx;
std::condition_variable cv;
int reader_count = 0;
bool writing = false;
public:
void lock_read()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this](){ return !writing; });
++reader_count;
}
void unlock_read()
{
std::unique_lock<std::mutex> lock(mtx);
if (--reader_count == 0)
cv.notify_all();
}
void lock_write()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this](){ return !writing && reader_count == 0; });
writing = true;
}
void unlock_write()
{
std::unique_lock<std::mutex> lock(mtx);
writing = false;
cv.notify_all();
}
};
RWLock rwLock;
int shared_data = 0;
void reader(int id)
{
for (int i = 0; i < 5; ++i)
{
rwLock.lock_read();
std::cout << "Reader " << id << " reads " << shared_data << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
rwLock.unlock_read();
}
}
void writer(int id)
{
for (int i = 0; i < 3; ++i)
{
rwLock.lock_write();
++shared_data;
std::cout << "Writer " << id << " writes " << shared_data << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
rwLock.unlock_write();
}
}
int main()
{
std::vector<std::thread> threads;
threads.emplace_back(writer, 1);
for (int i = 0; i < 3; ++i)
threads.emplace_back(reader, i);
for (auto &t : threads)
t.join();
}
这样写感觉还是有点麻烦,可以加上RALL思想封装一下
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <chrono>
class RWLock
{
private:
std::mutex mtx;
std::condition_variable cv;
int reader_count = 0;
bool writing = false;
public:
void lock_read()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this](){ return !writing; });
++reader_count;
}
void unlock_read()
{
std::unique_lock<std::mutex> lock(mtx);
if (--reader_count == 0)
cv.notify_all();
}
void lock_write()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this](){ return !writing && reader_count == 0; });
writing = true;
}
void unlock_write()
{
std::unique_lock<std::mutex> lock(mtx);
writing = false;
cv.notify_all();
}
};
class ReadGuard
{
public:
explicit ReadGuard(RWLock& lock) : rwLock(lock) // 避免隐式转换
{
rwLock.lock_read();
}
~ReadGuard()
{
rwLock.unlock_read();
}
private:
RWLock& rwLock;
};
class WriteGuard
{
public:
explicit WriteGuard(RWLock& lock) : rwLock(lock)
{
rwLock.lock_write();
}
~WriteGuard()
{
rwLock.unlock_write();
}
private:
RWLock& rwLock;
};
RWLock rwLock;
int shared_data = 0;
void reader(int id)
{
for (int i = 0; i < 5; ++i)
{
ReadGuard guard(rwLock);
{
printf("Reader id is : %d, shared data is : %d\n", id, shared_data);
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
void writer(int id)
{
for (int i = 0; i < 3; ++i)
{
WriteGuard guard(rwLock);
{
++shared_data;
printf("Writer id is : %d, shared data is : %d\n", id, shared_data);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
std::vector<std::thread> threads;
threads.emplace_back(writer, 1);
for (int i = 0; i < 3; ++i)
threads.emplace_back(reader, i);
for (auto &t : threads)
t.join();
}
5、自旋锁(spinlock)
C++ 中的**自旋锁(Spinlock)**是一种轻量级的锁,用于多线程同步。它与普通互斥锁(std::mutex)不同:
1、自旋锁不会让线程进入阻塞状态,而是不断"轮询"检查锁是否可用,直到获得锁或条件满足为止。
2、因此自旋锁适合锁持有时间非常短的场景,否则会浪费 CPU 资源。
3、通常用于多核 CPU 的短期临界区。
cpp
#include <atomic>
#include <thread>
#include <iostream>
class SpinLock
{
private:
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
// 获取锁(阻塞,忙等)
void lock()
{
while (flag.test_and_set(std::memory_order_acquire))
{
printf("Rotation waiting ...... \n");
}
}
// 尝试获取锁(非阻塞)
bool try_lock()
{
// 如果之前没有被占用,返回 true 并获取锁
return !flag.test_and_set(std::memory_order_acquire);
}
// 释放锁
void unlock()
{
flag.clear(std::memory_order_release);
}
};
// RAII 封装,自动加锁/解锁
class SpinLockGuard
{
private:
SpinLock &lock_;
public:
explicit SpinLockGuard(SpinLock &lock) : lock_(lock)
{
lock_.lock();
}
~SpinLockGuard()
{
lock_.unlock();
}
// 禁止拷贝
SpinLockGuard(const SpinLockGuard &) = delete;
SpinLockGuard &operator=(const SpinLockGuard &) = delete;
};
// 测试
int main()
{
SpinLock spin;
int counter = 0;
auto worker1 = [&]()
{
for (int i = 0; i < 1000; ++i)
{
SpinLockGuard guard(spin); // 自动加锁
++counter;
printf("counter + is : %d\n", counter);
}
};
auto worker2 = [&]()
{
for (int i = 0; i < 1000; ++i)
{
SpinLockGuard guard(spin); // 自动加锁
--counter;
printf("counter - is : %d\n", counter);
}
};
std::thread t1(worker1);
std::thread t2(worker2);
t1.join();
t2.join();
return 0;
}
6、一次性初始化锁
一次性初始化锁是多线程编程里的一种机制,保证某段初始化代码在多个线程竞争时只会执行一次,并且其它线程会安全地等待这次初始化完成。
在 C++ 里,它的正式用法就是:std::call_once + std::once_flag
cpp
#include <thread>
#include <iostream>
#include <mutex>
std::once_flag flag;
void initResource()
{
printf("资源初始化(只执行一次)\n");
}
void task(int id)
{
std::call_once(flag, initResource);
;
printf("线程 : %d, 继续执行\n", id);
}
int main()
{
std::thread t1(task, 1);
std::thread t2(task, 2);
std::thread t3(task, 3);
t1.join();
t2.join();
t3.join();
}
7、原子操作(无锁)
首先解释一下CPU乱序执行:C++ 乱序执行是CPU 和编译器为优化性能,在不改变单线程语义的前提下,重排指令执行顺序的行为,多线程场景下会导致数据竞争和可见性问题,需通过内存序、锁等机制约束。
cpp
// 线程1
int x = 0, y = 0;
void thread1() {
x = 1; // 无依赖,可能被重排到 y=1 之后
y = 1;
}
// 线程2
void thread2() {
while (y != 1); // 假设线程2看到 y=1
std::cout << x; // 可能输出 0(线程1的x=1被重排,未同步到线程2)
}
std::atomic 提供原子操作,并可以指定 内存序(memory_order) 来控制:
| 内存序 | 说明 | 特点 | 典型用法 |
|---|---|---|---|
memory_order_relaxed |
放宽顺序 | 仅保证原子性,不保证顺序或同步 | 计数器、性能优化 |
memory_order_consume |
数据依赖顺序 | 保证依赖于该原子的操作不会被重排序(不常用,支持有限) | 指针依赖同步 |
memory_order_acquire |
获取锁 | 保证此操作之前的读操作不会被重排序到它之后 | 读取锁或标志 |
memory_order_release |
释放锁 | 保证此操作之后的写操作不会被重排序到它之前 | 写入锁或标志 |
memory_order_acq_rel |
获取 + 释放 | 同时保证 acquire 和 release 的语义 | 读-改-写操作 |
memory_order_seq_cst |
顺序一致 | 全局单一顺序,最强保证,默认行为 | 大多数简单同步场景 |
7.1、memory_order_relaxed
memory_order_relaxed 表示:
1、原子性:对该原子变量的操作不会被线程打断,不会出现中间状态。
2、不保证顺序:编译器和 CPU 可以对操作进行重排序,前后的读写对其他线程没有顺序保证。
3、不保证同步:线程之间看不到操作的顺序关系,即一个线程对变量的修改可能对其他线程立即不可见。
换句话说:原子操作不会崩溃或损坏,但不同线程看到的顺序可能乱。
示例
cpp
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> x{0}, y{0};
void thread1() {
x.store(1, std::memory_order_relaxed); // 原子写
y.store(1, std::memory_order_relaxed); // 原子写
}
void thread2() {
while (y.load(std::memory_order_relaxed) == 0);
if (x.load(std::memory_order_relaxed) == 0) {
std::cout << "x is 0 but y is 1!" << std::endl;
}
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
期望:x=1 在 y=1 之前写入,因此 thread2 看到 y=1 时 x 应该是 1。
实际可能:由于 relaxed 没有顺序保证,CPU/编译器可能把 y 的写入提前到 x 之前,所以 thread2 有可能看到 y=1 但 x=0。
原子性保证:即使两个线程同时写 x 或 y,不会出现 "x=2" 这种错误状态。
7.2 memory_order_release
memory_order_release表示:
1、memory_order_release 通常用于 写操作。
2、保证 release 之前的所有写操作 对其他线程可见,并且不会被重排序到 release 之后。
cpp
// 生产者线程
void producer() {
data = 42; // 写入数据
ready.store(true, std::memory_order_release); // release 标志
}
保证在生产者线程执行中,data = 42; 一定执行在 ready.store(true, std::memory_order_release);之前。消费者线程如果通过ready来判断读取data的数值,一定能保证data = 42;
修改一下代码
cpp
void producer() {
data = 42; // 写入数据
data = 41;
data = 40;
data = 46;
ready.store(true, std::memory_order_release); // release 标志
}
memory_order_release只能保证ready.store(true, std::memory_order_release);在data的赋值操作完成后执行,但是不能保证data赋值操作的顺序,也就是说最终拿到的data不一定是46
7.3 memory_order_acquire
memory_order_acquire表示
1、acquire 之后的操作不会乱序到 acquire 之前。
2、对其他线程的写操作可见(如果对应 release 存在)。
cpp
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<bool> ready{false};
int data = 0;
// 生产者线程
void producer() {
data = 42; // 写入数据
ready.store(true, std::memory_order_release); // release 标志
}
// 消费者线程
void consumer() {
while (!ready.load(std::memory_order_acquire)); // acquire
// 保证看到 producer release 前写入的数据
std::cout << "data = " << data << std::endl; // 一定输出 42
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
假设生产者的操作如下:
cpp
void producer() {
data = 42;
data = 41;
data = 40;
data = 46; // 写入数据
ready.store(true, std::memory_order_release); // release 标志
}
使用memory_order_acquire也不能保证最终的结果是data = 46;
cpp
std::atomic<int> data_atomic;
data_atomic.store(42, std::memory_order_relaxed);
data_atomic.store(41, std::memory_order_relaxed);
data_atomic.store(40, std::memory_order_relaxed);
data_atomic.store(46, std::memory_order_release);
这样才能保证消费者最终拿到的数值为46
cpp
int value = data_atomic.load(std::memory_order_acquire);
std::cout << "data = " << value << std::endl; // 46
7.4 memory_order_consume
memory_order_consume表示:
1、memory_order_consume 保证 数据依赖顺序(data-dependent ordering)。
2、如果一个操作的值依赖于原子变量,那么这个操作不会被重排序到原子读取之前。
cpp
p = atomic_ptr.load(memory_order_consume)
*p ... // 访问 p 所指向的数据,保证这个访问不会乱序到 load 之前
3、不保证所有操作顺序,只有依赖于该原子变量的操作受到保证。
ps : 支持有限:很多编译器(如 GCC、Clang)在实现上退化为 memory_order_acquire,所以通常不建议单独依赖 consume。
cpp
#include <atomic>
#include <thread>
#include <iostream>
struct Data {
int value;
};
std::atomic<Data*> ptr{nullptr};
void producer() {
static Data data;
data.value = 42;
ptr.store(&data, std::memory_order_release); // release 写入指针
}
void consumer() {
Data* p = ptr.load(std::memory_order_consume); // consume 读取
if (p) {
// 保证访问 p->value 不会乱序到 load 之前
std::cout << "data.value = " << p->value << std::endl;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
7.5 memory_order_acq_rel
1、memory_order_acq_rel 是 读-改-写操作(read-modify-write) 常用的内存序。
2、它结合了 acquire 和 release 的语义:
release 语义:保证此操作之前的写操作对其他线程可见。
acquire 语义:保证此操作之后的读操作不会乱序到操作之前。
3、通常用于 无锁数据结构 的原子操作,例如 fetch_add, compare_exchange 等。
4、简单理解:一个操作既"发布"之前的写入,又"获取"其他线程的写入。
cpp
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> counter{0};
std::atomic<bool> ready{false};
int data = 0;
// 线程 1:修改数据并增加计数
void thread1() {
data = 42; // 普通写
counter.fetch_add(1, std::memory_order_acq_rel); // acq_rel 原子操作
ready.store(true, std::memory_order_release); // release 标志
}
// 线程 2:读取数据并检查计数
void thread2() {
while(!ready.load(std::memory_order_acquire)); // acquire
int c = counter.load(std::memory_order_acquire); // acquire 读取
std::cout << "data = " << data << ", counter = " << c << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
}
简单点说就是保证多个线程都在写时候,counter + 1,没有执行完成,另一个线程在写时候拿到的并不是最新的counter数值。
可以将本例中的counter理解为统计线程写入次数,如果不使用memory_order_acq_rel,是不是会出现。
cpp
counter = 0;
线程1 :counter++;
线程2 : counter++;
线程1未++完成的情况下,线程2也要执行++,导致两次++的结果都是1;
如果使用 memory_order_acq_rel 则能保证,线程2拿到的一定是++之后的数值。
7.6 memory_order_seq_cst
memory_order_seq_cst表示:
1、memory_order_seq_cst = Sequentially Consistent,即"顺序一致性"。
2、最严格的内存序,所有线程看到的原子操作 全局有统一顺序。
3、不仅保证 release/acquire 的可见性,还保证 所有原子操作在全局上有一致顺序。
4、默认情况下,C++ 的原子操作就是 seq_cst。
5、简单理解:全局操作按某种顺序排列,每个线程看到的顺序是一致的。
cpp
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> x{0};
std::atomic<int> y{0};
// 线程 A
void threadA() {
x.store(1, std::memory_order_seq_cst);
int r1 = y.load(std::memory_order_seq_cst);
std::cout << "Thread A reads y = " << r1 << std::endl;
}
// 线程 B
void threadB() {
y.store(1, std::memory_order_seq_cst);
int r2 = x.load(std::memory_order_seq_cst);
std::cout << "Thread B reads x = " << r2 << std::endl;
}
int main() {
std::thread t1(threadA);
std::thread t2(threadB);
t1.join();
t2.join();
}
编译器和 CPU 会保证:
x.store(1) 先于 y.load() 在某个全局顺序中执行。
y.store(1) 先于 x.load() 在某个全局顺序中执行。
可能的输出有:
Thread A reads y = 0, Thread B reads x = 1
Thread A reads y = 1, Thread B reads x = 0
结语
感谢您的阅读,如有问题可以私信或评论区交流。
^ _ ^