You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

写时复制(Copy-on-Write)如何保障线程安全?C++正确实现探讨

线程安全的写时复制(Copy-on-Write)实现方案

你指出的原代码的两个线程安全问题完全正确:

  • 检查m_data && m_data.unique()的过程是非原子的,若其他线程在此时修改引用计数,会导致判断逻辑出错——比如明明当前不是唯一持有者,却误以为是,直接修改共享数据;
  • rename中替换m_data和后续修改数据的操作之间没有同步,其他线程可能读到刚替换的空/默认数据,或者旧数据与新数据混合的异常状态。

要实现线程安全的COW,核心是确保所有对共享指针的读写操作都被同步保护,下面给出两种可行的实现方式:

方式一:使用互斥锁(兼容C++11及以上)

这是最直观且兼容性最好的方案,通过互斥锁确保同一时间只有一个线程能访问或修改共享指针:

#include <iostream>
#include <string>
#include <memory>
#include <mutex>

using namespace std;

struct SPersonData
{
    string m_name;
    string m_surname;
};

class CPerson
{
private:
    mutable mutex m_mutex; // mutable允许const方法中加锁
    shared_ptr<SPersonData> m_data;

public:
    string getName() const;
    string getSurname() const;
    void rename(const string&, const string&);
};

string CPerson::getName() const
{
    lock_guard<mutex> lock(m_mutex); // 加锁,作用域结束自动释放
    if (m_data) return m_data->m_name;
    return string();
}

string CPerson::getSurname() const
{ 
    lock_guard<mutex> lock(m_mutex);
    if (m_data) return m_data->m_surname;
    return string();
}

void CPerson::rename(const string &name, const string &surname)
{
    lock_guard<mutex> lock(m_mutex);
    // 检查是否需要复制:若指针为空或不是唯一持有者,创建新副本
    if (!m_data || !m_data.unique()) {
        auto new_data = make_shared<SPersonData>();
        // 可选:拷贝旧数据再修改,保留未改动字段(根据业务需求调整)
        if (m_data) {
            *new_data = *m_data;
        }
        m_data = move(new_data);
    }
    // 此时m_data是唯一持有者,安全修改数据
    m_data->m_name = name;
    m_data->m_surname = surname;
}

关键修改点:

  • 新增mutable mutex m_mutex:因为getName和getSurname是const方法,mutable允许在const方法中修改锁对象;
  • 所有访问m_data的方法都用lock_guard加锁:确保读写操作互斥,彻底避免并发竞态;
  • 复制时可选拷贝旧数据:如果业务要求修改时保留其他未修改字段,可添加此逻辑(原代码未实现,可按需调整)。

方式二:使用原子化的shared_ptr(C++20及以上)

C++20引入了std::atomic<std::shared_ptr>,可以直接对shared_ptr进行原子操作,无需手动加锁:

#include <iostream>
#include <string>
#include <memory>
#include <atomic>

using namespace std;

struct SPersonData
{
    string m_name;
    string m_surname;
};

class CPerson
{
private:
    atomic<shared_ptr<SPersonData>> m_data;

public:
    string getName() const;
    string getSurname() const;
    void rename(const string&, const string&);
};

string CPerson::getName() const
{
    auto data = m_data.load(); // 原子加载当前指针
    if (data) return data->m_name;
    return string();
}

string CPerson::getSurname() const
{ 
    auto data = m_data.load();
    if (data) return data->m_surname;
    return string();
}

void CPerson::rename(const string &name, const string &surname)
{
    while (true) {
        auto current_data = m_data.load();
        // 检查是否需要复制
        if (current_data && current_data.unique()) {
            // 是唯一持有者,直接修改数据
            current_data->m_name = name;
            current_data->m_surname = surname;
            break;
        }
        // 需要创建新副本
        auto new_data = make_shared<SPersonData>();
        if (current_data) {
            *new_data = *current_data;
            new_data->m_name = name;
            new_data->m_surname = surname;
        } else {
            new_data->m_name = name;
            new_data->m_surname = surname;
        }
        // 原子替换:只有当前指针和current_data一致时才替换,避免竞态
        if (m_data.compare_exchange_weak(current_data, new_data)) {
            break;
        }
        // 替换失败说明其他线程修改了指针,重新循环尝试
    }
}

关键说明:

  • 使用std::atomic<shared_ptr<SPersonData>>存储共享指针,所有读写操作都是原子的;
  • rename中用compare_exchange_weak实现无锁的原子替换:如果当前指针和加载的current_data一致,就替换成新指针,否则重新尝试,避免竞态;
  • 这种方式是无锁实现,性能比互斥锁更高,但逻辑更复杂,且仅支持C++20及以上版本。

核心原则总结

线程安全的COW必须满足:

  1. 所有对共享指针的访问(读/写)都必须同步:要么用互斥锁保护,要么用原子操作;
  2. 复制和替换指针的操作必须是原子的:不能出现判断和替换之间的间隙;
  3. 修改数据前必须确保是唯一持有者:只有当指针是唯一引用时,才能直接修改,否则必须创建副本。

内容的提问来源于stack exchange,提问作者just_code_dog

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 14:47:32