7

我目前正在尝试将单线程程序传递给多线程。该软件大量使用“refCounted”对象,这导致了多线程中的一些问题。我正在寻找一些设计模式或可能解决我的问题的东西。

主要问题是线程之间的对象删除,通常删除只会减少引用计数,当 refcount 等于 0 时,则删除对象。这在单线程程序中运行良好,并且可以通过复制大对象来提高性能。

但是,在多线程中,两个线程可能希望同时删除同一个对象,因为该对象受互斥锁保护,只有一个线程删除该对象并阻止另一个。但是当它释放互斥锁时,另一个线程继续执行无效(释放的对象),这会导致内存损坏。

这是此类 RefCountedObject 的示例

class RefCountedObject
{
public:
RefCountedObject()
:   _refCount( new U32(1) )
{}

RefCountedObject( const RefCountedObject& obj )
:   _refCount( obj._refCount )
{
    ACE_Guard< ACE_Mutex > guard( _refCountMutex );
    ++(*_refCount);
}

~RefCountedObject()
{
    Destroy();
}

RefCountedObject& operator=( const RefCountedObject& obj )
{
    if( this != &obj )
    {
        Destroy();
        ACE_Guard< ACE_Mutex > guard( _refCountMutex );
        _refCount = obj._refCount;
        ++(*_refCount);
    }

    return *this;
}

private:
    void Destroy()
    {
        ACE_Guard< ACE_Mutex > guard( _refCountMutex );  // thread2 are waiting here
        --(*_refCount);         // This cause a free memory write by the thread2
        if( 0 == *_refCount )
            delete _refCount;
    }

private:
    mutable U32* _refCount;
    mutable ACE_Mutex _refCountMutex; // BAD: this mutex only protect the refCount pointer, not the refCount itself
};

假设两个线程要删除同一个 RefCountedObject,都在 ~RefCountedObject 中并调用 Destroy(),第一个线程已锁定互斥体,另一个正在等待。在第一个线程删除对象后,第二个线程将继续执行并导致空闲内存写入。

任何人都有类似问题的经验并找到了解决方案?


感谢大家的帮助,我意识到我的错误:互斥锁只保护 refCount 指针,而不是 refCount 本身!我创建了一个受互斥保护的 RefCount 类。互斥锁现在在所有 refCounted 对象之间共享。

现在一切正常。

4

7 回答 7

4

如果计数是对象的一部分,那么如果一个线程可能试图增加引用计数而另一个线程试图删除最后一个引用,那么您就会遇到一个固有的问题。每个指向对象的全局可访问指针的引用计数都需要一个额外的值,因此如果你有一个指针,你总是可以安全地增加引用计数。

一种选择是使用boost::shared_ptr (参见文档)。在访问指向共享对象的全局指针时,您可以使用自由函数atomic_load、和(文档中明显没有这些函数)来确保适当的保护。一旦您的线程获得了对特定对象的引用,您就可以使用普通的非原子函数来访问它。atomic_storeatomic_exchangeatomic_compare_exchangeshared_ptr

另一种选择是使用 Joe Seigh 的atomic_ptr_plus 项目中的 atomic ref-counted pointer

于 2008-10-03T10:24:12.023 回答
3

当然,每个线程只需要正确管理引用计数......也就是说,如果 ThreadA 和 ThreadB 都与 Obj1 一起工作,那么 ThreadA 和 ThreadB 都应该拥有对该对象的引用,并且当它们完成时都应该调用 release目的。

在单线程应用程序中,您可能会创建一个引用计数对象,然后您对该对象进行处理并最终调用 release。在多线程程序中,您将创建对象,然后将其传递给您的线程(无论如何您都这样做)。在将对象传递给线程之前,您应该在对象上调用 AddRef() 以赋予线程自己的引用计数。分配对象的线程然后可以调用释放,因为它已经完成了对象。使用该对象的线程将在完成后调用 release,当最后一个引用被释放时,该对象将被清理。

请注意,您不希望在线程本身上运行的代码在对象上调用 AddRef(),因为在您调度的线程之前,在对象上创建线程调用 release 之间存在竞争条件以获得机会运行并调用 AddRef()。

于 2008-10-03T09:54:12.943 回答
1

稍微考虑一下您的问题...您的意思是您有 1 个对象(如果 refcount 为 1),但 2 个线程都在其上调用 delete()。我认为这才是你真正的问题所在。

解决此问题的另一种方法是,如果您想要一个可以在线程之间安全重用的线程对象,则在释放内部内存之前检查引用计数是否大于 1。目前你释放它,然后检查 refcount 是否为 0。

于 2008-10-03T10:25:29.760 回答
1

这不是答案,只是一点建议。在这种情况下,在你开始修复任何东西之前,请确保你可以可靠地复制这些问题。有时这很简单,就像在循环中运行单元测试一段时间一样。有时在你的程序中加入一些聪明的睡眠来强制竞争条件是有帮助的。

参考计数问题往往会持续存在,因此从长远来看,对测试工具的投资将获得回报。

于 2008-10-03T15:39:11.443 回答
0

您在线程之间共享的任何对象都应使用互斥锁进行保护,这同样适用于引用计数句柄!这意味着您永远不会从两个线程中删除对象的最后一个句柄。您可能会同时删除恰好指向一个对象的两个不同句柄。

在 Windows 中,您可以使用 InterlockedDecrement。这确保了两个减量之一将返回 0。只有该线程将删除引用计数的对象。

任何其他线程也不能复制两个句柄之一。根据常见的 MT 规则,一个线程可能不会删除另一个线程仍在使用的对象,这也扩展到 refcount 句柄。

于 2008-10-03T09:57:19.380 回答
0

一种解决方案是使引用计数器成为原子值,以便每个并发调用destroy 可以安全地进行,实际上只发生1 次删除,另一种只是减少原子引用计数。

英特尔线程构建模块库 (TBB) 提供原子值。

此外,ACE_Atomic_Op模板中的 ACE 库也是如此。

Boost 库提供了一个引用计数智能指针库,它已经实现了这一点。

http://www.dre.vanderbilt.edu/Doxygen/Current/html/ace/a00029.html http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm

于 2008-10-03T10:02:58.247 回答
0

我相信沿着这条线的东西会解决你的问题:

private:
    void Destroy()
    {
ACE_Guard< ACE_Mutex > guard( _refCountMutex ); // thread2 are waiting here if (_refCount != 0) { --(*_refCount); // This cause a free memory write by the thread2 if( 0 == *_refCount ) { delete _refCount; _refcount = 0; } } } private: mutable U32* _refCount; mutable ACE_Mutex _refCountMutex;

于 2008-10-03T10:11:42.143 回答