C ++:多线程和引用对象

时间:2020-03-06 15:03:59  来源:igfitidea点击:

我目前正在尝试将单线程程序传递给多线程。该软件大量使用" refCounted"对象,这会导致多线程中的某些问题。我正在寻找某种设计模式或者某种可以解决我的问题的方法。

主要问题是线程之间的对象删除,通常删除仅减少引用计数,并且当refcount等于零时,将删除对象。这在单线程程序中效果很好,并且可以通过复制大对象来提高性能。

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

这是此类CountCountedObject的示例

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对象之间共享。

现在一切正常。

解决方案

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

在单线程应用程序中,很可能我们已经创建了一个引用计数对象,然后对该对象进行处理并最终调用release。在多线程程序中,我们将创建对象,然后将其传递给线程(但是我们要这样做)。在将对象传递给线程之前,应在对象上调用AddRef()以为线程提供自己的引用计数。然后,分配对象的线程可以像处理对象一样调用release。处理完对象的线程将在完成后调用release,并在释放最后一个引用时将清理对象。

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

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

在Windows中,我们可以使用InterlockedDecrement。这样可以确保两个减量之一精确地返回0。只有该线程才会删除被引用的对象。

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

一种解决方案是使引用计数器成为原子值,以便每次并发销毁调用都可以安全地继续进行实际发生的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

我相信,遵循这条原则可以解决问题:

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;

如果计数是对象的一部分,那么如果一个线程试图增加引用计数而另一个线程试图删除最后一个引用,则我们将遇到一个固有的问题。对于该对象的每个全局可访问指针,在ref计数上都需要有一个额外的值,因此,如果我们有一个指针,则始终可以安全地增加ref计数。

一种选择是使用boost :: shared_ptr(参见文档)。我们可以使用免费功能" atomic_load"," atomic_store"," atomic_exchange"和" atomic_compare_exchange"(在文档中明显缺少)来确保在访问指向共享对象的全局指针时得到适当的保护。一旦线程获得了一个指向特定对象的" shared_ptr",我们就可以使用常规的非原子函数来访问它。

另一种选择是使用Joe Seigh的atomic_ptr_plus项目中的原子引用计数指针

考虑一下问题...意思是说我们有1个对象(如果refcount为1),但是有2个线程都调用了delete()。我认为这是我们真正的问题所在。

解决此问题的另一种方法是,如果要使用线程对象,可以在线程之间安全地重用,则是在释放内部内存之前检查refcount是否大于1. 当前,我们将其释放,然后检查引用计数是否为0。

这不是答案,只是一些建议。在这种情况下,开始修复任何问题之前,请确保可以可靠地复制这些问题。有时,这很简单,因为我们可以在一个循环中运行单元测试一段时间。有时在程序中添加一些聪明的睡眠以强制比赛条件会有所帮助。

引用计数问题往往会持续存在,因此从长远来看,对测试工具的投资会有所回报。