【问题标题】:Atomicness of copy constructor of reference count object using InterlockedIncrement64使用 InterlockedIncrement64 的引用计数对象的复制构造函数的原子性
【发布时间】:2012-09-14 16:18:44
【问题描述】:

我正在努力思考如何确保对象引用计数是线程安全的。

class MyObject{
  //Other implementation details
private:
  mutable volatile LONGLONG * m_count;
  IData * m_data;
};

假设有必要的类声明,保持简单。下面是拷贝构造函数和析构函数的实现。

MyObject::MyObject(const MyObject& rhs) : m_count(rhs.m_count), m_data(rhs.m_data){
    InterlockedIncrement64(m_count);
}

MyObject::~MyObject(){
    if(InterlockedDecrement64(m_count) == 0)
    delete m_data;
}

这个线程安全吗?复制构造函数的 intilization 列表是如何看到的,原子的还是非原子的?这还重要吗?我应该在初始化列表中设置计数的递增值(这可能吗)?

就目前而言,这已经足够好了。我在想,否则,我怎么会遇到thread1 正在复制而thread2 正在同时销毁count == 1 的情况。线程之间必须进行握手,这意味着线程 1 必须在线程 2 的对象超出范围之前完全复制对象,对吗?

在阅读了其中一些回复后,我回去做了一些研究。 Boost 非常相似地实现了它们的 shared_ptr。这是析构函数调用。

void release() // nothrow
{
    if( BOOST_INTERLOCKED_DECREMENT( &use_count_ ) == 0 )
    {
        dispose();
        weak_release();
    }
}

有些人建议在 boost 文档中明确指出分配不是线程安全的。我同意和不同意。我认为在我的情况下我不同意。我只需要threadA和threadB之间的握手。我认为某些回复中描述的某些问题在这里并不适用(尽管它们是我没有完全考虑透彻的令人大开眼界的回复)。

示例 线程A 附加(共享对象); //通过值传递的共享对象,计数递增等。

线程B //接受对象,将其添加到共享对象列表中。 ThreadB 位于一个计时器上,该计时器向所有 SharedObjects 通知一个事件。在通知之前,列表的副本由关键部分保护。 CS发布,副本通知。

线程A 分离(共享对象); //从对象列表中删除共享对象

现在,ThreadB 正在同时对 SharedOjbect 进行单选,并且在 ThreadA 分离所述共享对象之前已经制作了列表的副本。一切都好吗?

【问题讨论】:

  • 为什么是 64 位引用计数?你确定你没有过度设计这个吗?
  • 如果你只是想保护柜台,你可以看看std::atomic,如果你想要更广泛的保护,你可以看看std::mutex
  • 我没有发现代码有任何问题。

标签: c++ copy-constructor atomic interlocked-increment


【解决方案1】:

从技术上讲,它应该是安全的。

为什么?因为为了复制一个对象,源需要有一个“引用”,所以它不会在复制过程中消失。此外,没有人正在访问当前正在构建的对象。

析构函数也是安全的,因为无论如何都没有留下任何引用。

不过,您可能需要重新考虑复制引用计数。这些引用实际上并不存在。每个参考原件的人都必须以某种方式减少副本的引用计数,前提是它在复制之前获得了原始参考。副本应该像新对象一样开始,引用计数为 1。

编辑:同样,如果你正在实现一个赋值运算符(就像一个现有对象的副本),目标对象的引用计数应该保持原样。

【讨论】:

  • 你错了,没有什么能阻止另一个线程在初始化列表和ctor body执行之间删除全局对象。
  • @Rost:那将是与此代码无关的另一个错误。只要不相关的代码没有不相关的错误,此代码就是安全的。如果您将对对象的引用传递给另一个函数,则您有责任确保该对象存在,直到该函数返回。 (否则,任何成员函数都不会是线程安全的,因为该对象可能在执行该函数(即 UB)期间被销毁。)他的代码不必修复其调用者中的错误,也不能。
  • @DavidSchwartz 不是在线程安全方面。这不是错误,它是预期行为 - 否则不需要使用原子。
  • @Rost:需要原子,因为另一个线程可能会在此函数增加引用计数时增加或减少引用计数。但它不能将其降为零,因为调用者持有一个引用。
  • 我同意 Rost 的观点,这不安全。引用是防止某些内容被删除的神奇事物。即使在单线程应用程序中,也很容易引用不再存在的东西。当然,未定义的行为,但很容易调用。如果魔法在单线程应用程序中不起作用,是什么让你觉得它突然在多线程应用程序中起作用了?
【解决方案2】:

构造函数不安全,因为初始化列表不是原子的(我在标准中没有找到任何对此的引用,但无论如何它都很难实现)。

因此,如果另一个线程将删除当前线程当前复制的对象 - 就在初始化列表执行和 InterlockedIncrement() 执行之间 - 你将收到损坏的(已删除 m_datam_datam_count。这将导致至少两次删除m_data

InterlockedIncrement 放在初始化列表中并没有帮助,因为线程切换可能发生在ctor 调用之后但m_count 初始化之前。

我不确定是否可以在没有外部锁(互斥锁或临界区)的情况下使其线程安全。您至少可以检查 ctor 中的计数器并在它为零时抛出异常/创建“无效”对象,但这不是好的设计,我不推荐它。

【讨论】:

  • 另一个线程无法删除该对象,因为该线程持有对它的引用。调用此函数的任何函数都在调用此对象的成员函数,因此它必须持有对它的引用。调用者有责任确保对象在对对象进行操作时保持存在。按照这种逻辑,没有任何成员函数是安全的,因为在执行该成员函数期间对象总是可能被销毁。
  • 右:如果源对象与此构造函数位于不同的线程中,则在此构造函数复制指针值之后和在线程切换可能导致引用计数递增之前会有一个短窗口源对象的析构函数将引用计数减为 0 并销毁托管对象。
  • @DavidSchwartz - 一般而言可能。但是,当您编写一个旨在避免此类问题的包装类时,您不能假设此类问题不会发生。 “这是线程安全的,除非你搞砸了”与“这不是线程安全的”相同。
  • +1,不安全。仅仅因为if 测试是原子的,并不会使整个if 块成为原子的。需要使整个块原子化的是程序员,而不是 C++。
  • @DavidSchwartz - 复制构造函数通常不是线程安全的,正是因为当被复制的对象位于与复制的线程不同的线程中时,可以在复制构建完成之前销毁源对象。通常,该问题的解决方案是“不要那样做”。但是当类的目的是确保线程安全时,“不要那样做”意味着它失败了。
【解决方案3】:

只要调用代码确保通过引用传递的对象在此函数执行期间不被破坏,此代码就是安全的。这对于任何需要引用的函数都是一样的,你必须非常努力才能不这样做。

析构函数是安全的,因为原子减量保证在一个且只有一个线程中为零。如果是,那肯定是其他所有线程都已经完成了对对象的使用,并且已经调用了自己的减量操作。

这假设您的联锁操作都有完整的障碍。

当计数 == 1 时,我怎么会遇到线程 1 正在复制而线程 2 正在同时销毁的情况。线程之间必须进行握手,这意味着线程 1 必须在线程 2 的对象退出之前完全复制对象范围正确吗?

你不能,只要每个线程都有自己的对象引用。只要 thread1 对对象有自己的引用,就不能在 thread1 复制时销毁该对象。 Thread1 不需要在 thread2 的引用消失之前复制,因为除非你有对它的引用,否则你永远不会触摸一个对象。

单个引用的线程安全性较弱,不应在不同线程中同时访问。如果两个线程想要访问同一个对象,它们应该都有自己的引用。在将对象引用到其他代码(可能在另一个线程中)时,请遵循以下操作顺序:

  1. 有自己的参考。

  2. 根据您自己的参考为其他代码创建参考。

  3. 现在您可以销毁您的参考资料或放弃其他参考资料。

【讨论】:

  • 如果我已经将对Object 的引用传递到另一个线程,我必须保留原始对象多长时间才能确保在构建副本期间它不会被破坏另一个线程?
  • @PeteBecker:创建其他线程引用的代码可能会在完成创建新引用后立即销毁自己的引用。 1)有参考。 2)为其他线程创建引用。 3) 销毁自己的引用。
  • @PeteBecker:实际上只有一个规则:只要对对象进行操作,任何对对象进行操作的代码都有责任确保它持有对该对象的引用. 要传递引用,您必须创建要传递的引用,这是对对象的操作。所以你必须持有一个引用,直到你完成创建你将给另一个线程的引用。
【解决方案4】:

你的拷贝构造函数不安全,不能保证安全。

但是,如果您从不使用 new/delete,您可以安全地使用您的类,而只使用自动创建和销毁的对象(按范围)。

【讨论】:

  • 解释一下会很有帮助。
猜你喜欢
  • 1970-01-01
  • 2020-04-30
  • 1970-01-01
  • 2018-11-19
  • 2018-08-14
  • 1970-01-01
  • 1970-01-01
  • 2014-01-05
  • 1970-01-01
相关资源
最近更新 更多