在C ++,x86-64中读写线程安全的智能指针

时间:2011-11-04 09:30:34

标签: c++ thread-safety x86 smart-pointers lock-free

我开发了一些无锁数据结构,并出现以下问题。

我有编写线程在堆上创建对象并使用引用计数器将它们包装在智能指针中。我也有很多读者线程,可以使用这些对象。代码可能如下所示:

SmartPtr ptr;

class Reader : public Thread {
    virtual void Run {
       for (;;) {
           SmartPtr local(ptr);
           // do smth   
       }
    }   
};

class Writer : public Thread {
    virtual void Run {
       for (;;) {
           SmartPtr newPtr(new Object);    
           ptr = newPtr;  
       }
    }
};

int main() {
    Pool* pool = SystemThreadPool();
    pool->Run(new Reader());
    pool->Run(new Writer());
    for (;;) // wait for crash :(
}

当我创建ptr的线程本地副本时,它至少意味着

  1. 阅读地址。
  2. 增量参考计数器。
  3. 我无法以原子方式执行这两项操作,因此有时我的读者会使用已删除的对象。

    问题是 - 我应该使用什么样的智能指针来从几个线程进行读写访问,并且可以进行正确的内存管理?解决方案应该存在,因为Java程序员甚至不关心这样的问题,只是依赖于所有对象都是引用,只有在没有人使用它们时才会被删除。

    对于PowerPC,我发现http://drdobbs.com/184401888,看起来不错,但使用了我们在x86中没有的Load-Linked和Store-Conditional指令。

    据我所知,boost指针仅使用锁提供此类功能。我需要无锁解决方案。

4 个答案:

答案 0 :(得分:9)

boost :: shared_ptr有atomic_store,它使用一个“无锁”的自旋锁,对于99%的可能情况应该足够快。

    boost::shared_ptr<Object> ptr;
class Reader : public Thread {
    virtual void Run {
       for (;;) {
           boost::shared_ptr<Object> local(boost::atomic_load(&ptr));
           // do smth   
       }
    }   
};

class Writer : public Thread {
    virtual void Run {
       for (;;) {
           boost::shared_ptr<Object> newPtr(new Object);    
           boost::atomic_store(&ptr, newPtr);
       }
    }
};

int main() {
    Pool* pool = SystemThreadPool();
    pool->Run(new Reader());
    pool->Run(new Writer());
    for (;;)
}

编辑:

在回应下面的评论时,实施是在“boost / shared_ptr.hpp”......

template<class T> void atomic_store( shared_ptr<T> * p, shared_ptr<T> r )
{
    boost::detail::spinlock_pool<2>::scoped_lock lock( p );
    p->swap( r );
}

template<class T> shared_ptr<T> atomic_exchange( shared_ptr<T> * p, shared_ptr<T> r )
{
    boost::detail::spinlock & sp = boost::detail::spinlock_pool<2>::spinlock_for( p );

    sp.lock();
    p->swap( r );
    sp.unlock();

    return r; // return std::move( r )
}

答案 1 :(得分:4)

对于一些jiggery-pokery,您应该能够使用InterlockedCompareExchange128完成此任务。将引用计数和指针存储在2元素__int64数组中。如果引用计数在数组[0]中,而指针在数组[1]中,原子更新将如下所示:

while(true)
{
    __int64 comparand[2];
    comparand[0] = refCount;
    comparand[1] = pointer;
    if(1 == InterlockedCompareExchange128(
        array,
        pointer,
        refCount + 1,
        comparand))
    {
        // Pointer is ready for use. Exit the while loop.
    }
}

如果您的编译器没有InterlockedCompareExchange128内部函数,那么您可以使用底层的CMPXCHG16B指令代替,如果您不介意使用汇编语言进行处理。

答案 2 :(得分:2)

RobH提出的解决方案不起作用。它与原始问题具有相同的问题:访问引用计数对象时,它可能已被删除。

我在没有全局锁定(如boost :: atomic_store)或条件读/写指令的情况下解决问题的唯一方法是以某种方式延迟对象(或共享引用计数对象)的破坏,如果这样的话用过的)。所以zennehoy有一个好主意,但他的方法太不安全了。

我可能这样做的方法是保留编写器线程中所有指针的副本,以便编写者可以控制对象的销毁:

class Writer : public Thread {
    virtual void Run() {
        list<SmartPtr> ptrs; //list that holds all the old ptr values        

        for (;;) {
            SmartPtr newPtr(new Object);
            if(ptr)
                ptrs.push_back(ptr); //push previous pointer into the list
            ptr = newPtr;

            //Periodically go through the list and destroy objects that are not
            //referenced by other threads
            for(auto it=ptrs.begin(); it!=ptrs.end(); )
                if(it->refCount()==1)
                    it = ptrs.erase(it);
                else
                    ++it;
       }
    }
};

但是仍然需要智能指针类。这不适用于shared_ptr,因为读取和写入不是原子的。它几乎与boost :: intrusive_ptr一起使用。 intrusive_ptr上的赋值实现如下(伪代码):

//create temporary from rhs
tmp.ptr = rhs.ptr;
if(tmp.ptr)
    intrusive_ptr_add_ref(tmp.ptr);

//swap(tmp,lhs)
T* x = lhs.ptr;
lhs.ptr = tmp.ptr;
tmp.ptr = x;

//destroy temporary
if(tmp.ptr)
    intrusive_ptr_release(tmp.ptr);

据我所知,这里唯一缺少的是lhs.ptr = tmp.ptr;之前的编译器级别内存栅栏。添加完成后,读取rhs和写lhs在严格条件下都是线程安全的:1)x86或x64架构2)原子引用计数3)rhs引用计数不得转到在赋值期间为零(由上面的Writer代码保证)4)只有一个线程写入lhs(使用CAS可以有多个写入器)。

无论如何,您可以基于intrusive_ptr创建自己的智能指针类并进行必要的更改。绝对比重新实现shared_ptr更容易。此外,如果你想要表现,那么侵入性就是你要走的路。

答案 3 :(得分:-1)

这在java中更容易工作的原因是垃圾收集。在C ++中,您必须手动确保当您想删除某个值时,该值不会开始被其他线程使用。

我在类似情况下使用的解决方案是简单地延迟删除值。我创建了一个单独的线程,它遍历要删除的事物列表。当我想删除某些内容时,我将其添加到带有时间戳的列表中。删除线程在实际删除该值之前等待此时间戳之后的某个固定时间。您只需确保延迟足够大,以确保任何临时使用的值都已完成。

在我的情况下,100毫秒已经足够了,我选择了几秒钟才能安全。