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

C++ std::shared_ptr自定义删除器线程安全与调用时机问题

问题结论

不能在当前代码无额外约定的前提下,直接认定自定义删除器调用时所有A::update()操作已执行完成,是否安全完全取决于SomeConcurrentMap的实现语义和getA()的正确性。


问题背景

[编辑补充:以下为更具体的示例]

假设我有一个线程安全对象(所有公有成员均通过mutex保护),以及一个带自定义删除器的shared_ptr,代码示例如下:

class A {
public:
  void update(int x);
  void print_sum();
  ...
};

class AContainer {
private:
    SomeConcurrentMap<string, shared_ptr<A>> aMap;
public:
    void newA(string name) {
        aMap.emplace(name, shared_ptr<A>(new A, [](A *p){
            p->print_sum();
            delete p;
        }));
    }
    void finalizeA(string name) {
        aMap.erase(name);
    }
    shared_ptr<A> getA(string name) const {
        // fixme 处理键不存在的场景...
        return aMap.find(name).second;
    }
};

void someFunctionInSomeThread(const AContainer &cont, string name, int c) {
    // fixme 处理键不存在的场景...
    cont.getA(name)->update(c);
}

已知前提:

  • 类A的所有操作都受内部mutex保护
  • SomeConcurrentMap本身是线程安全的

使用场景:

  • 可从任意线程调用AContainer::newA()
  • 多个线程可多次调用someFunctionInSomeThread()
  • 可从任意线程调用AContainer::finalizeA(),该操作可能与上述someFunctionInSomeThread()的调用并行执行

预期目标:A::print_sum()会在finalizeA()执行完成、且所有正在运行的A::update()操作全部结束后才被调用。
核心疑问:是否可以安全认定,当自定义删除器中p->print_sum()被调用时,目标对象上的所有A::update()操作都已经执行完成?


详细分析

满足安全要求的前提

如果SomeConcurrentMap符合线程安全容器的通用设计约定,getA()能安全返回目标shared_ptr的独立副本——也就是容器保证,在调用方完成对返回值的拷贝构造之前,对应key的节点不会被并发的erase()操作销毁,那么结论是成立的,原因如下:

  • 所有调用update()的线程,必须先通过getA()拿到一个独立的shared_ptr<A>副本,这一过程会让shared_ptr的引用计数+1,只要该副本存活,对象就不会被销毁。
  • 根据C++标准,getA()返回的临时shared_ptr的生命周期会持续到包含它的全表达式(即cont.getA(name)->update(c)整行代码)执行结束,也就是update()调用返回后,临时shared_ptr才会析构,引用计数-1。
  • shared_ptr的引用计数增减是原子操作,自带同步语义:当最后一个持有对象的shared_ptr析构、引用计数归0时才会执行自定义删除器,所有之前的shared_ptr拷贝、析构操作都和删除器的调用存在happens-before关系。此时所有正在运行的update()必然已经执行完成,删除器中调用print_sum()完全符合预期目标。

存在竞态风险的场景

当前代码没有对SomeConcurrentMap的find()语义做明确约定,getA()的写法存在触发未定义行为的可能:
如果SomeConcurrentMap::find()仅在查找节点的过程中持有内部锁,找到节点后立刻释放锁,再返回指向节点内部shared_ptr的引用,那么后续拷贝shared_ptr构造返回值的过程是没有锁保护的。此时如果其他线程并发调用erase()销毁对应节点,就会出现悬空引用,拷贝shared_ptr的行为完全不可控,属于典型的数据竞争。
这种未定义行为可能导致引用计数没有正确累加,出现对象提前销毁、删除器被调用时仍有线程在执行update()的问题,完全无法保证预期逻辑生效。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 07:51:26