万字C++中锁机制和内存序详解

目录

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_guardunique_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::mutextry_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

结语

感谢您的阅读,如有问题可以私信或评论区交流。

^ _ ^

相关推荐
panzer_maus6 分钟前
归并排序的简单介绍
java·数据结构·算法
獭.獭.6 分钟前
C++ -- STL【unordered_set和unordered_map的使用】
c++·stl·unordered_map·unordered_set
Jelena1577958579222 分钟前
Java爬虫淘宝拍立淘item_search_img拍接口示例代码
开发语言·python
郝学胜-神的一滴35 分钟前
Python数据模型:深入解析及其对Python生态的影响
开发语言·网络·python·程序人生·性能优化
一水鉴天42 分钟前
整体设计 定稿 之26 重构和改造现有程序结构 之2 (codebuddy)
开发语言·人工智能·重构·架构
cici158741 小时前
二值化断裂裂缝的智能拼接算法
人工智能·算法·计算机视觉
麦格芬2301 小时前
LeetCode 763 划分字母区间
算法·leetcode·职场和发展
star _chen1 小时前
C++ std::move()详解:从小白到高手
开发语言·c++
lzhdim1 小时前
C#开发者必知的100个黑科技(前50)!从主构造函数到源生成器全面掌握
开发语言·科技·c#
福尔摩斯张1 小时前
C++核心特性精讲:从C语言痛点出发,掌握现代C++编程精髓(超详细)
java·linux·c语言·数据结构·c++·驱动开发·算法