简体   繁体   English

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

[英]Read-write thread-safe smart pointer in C++, x86-64

I develop some lock free data structure and following problem arises. 我开发了一些无锁数据结构,并出现以下问题。

I have writer thread that creates objects on heap and wraps them in smart pointer with reference counter. 我有编写器线程在堆上创建对象并使用引用计数器将它们包装在智能指针中。 I also have a lot of reader threads, that work with these objects. 我也有很多读者线程,可以使用这些对象。 Code can look like this: 代码可能如下所示:

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 :(
}

When I create thread-local copy of ptr it means at least 当我创建ptr线程局部副本时,它至少意味着

  1. Read an address. 读一个地址。
  2. Increment reference counter. 增加参考计数器。

I can't do these two operations atomically and thus sometimes my readers work with deleted object. 我无法原子地执行这两个操作,因此有时候我的读者会使用已删除的对象。

The question is - what kind of smart pointer should I use to make read-write access from several threads with correct memory management possible? 问题是 - 我应该使用什么样的智能指针来从几个线程进行读写访问,并且可以进行正确的内存管理? Solution should exist, since Java programmers don't even care about such a problem, simply relying on that all objects are references and are deleted only when nobody uses them. 解决方案应该存在,因为Java程序员甚至不关心这样的问题,只是依赖于所有对象都是引用,只有在没有人使用它们时才会被删除。

For PowerPC I found http://drdobbs.com/184401888 , looks nice, but uses Load-Linked and Store-Conditional instructions, that we don't have in x86. 对于PowerPC,我发现http://drdobbs.com/184401888 ,看起来不错,但使用了我们在x86中没有的Load-Linked和Store-Conditional指令。

As far I as I understand, boost pointers provide such functionality only using locks. 据我所知,boost指针仅使用锁提供此类功能。 I need lock free solution. 我需要无锁解决方案。

boost::shared_ptr have atomic_store which uses a "lock-free" spinlock which should be fast enough for 99% of possible cases. 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 (;;)
}

EDIT: 编辑:

In response to comment below, the implementation is in "boost/shared_ptr.hpp"... 在回应下面的评论时,实施是在“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 )
}

With some jiggery-pokery you should be able to accomplish this using InterlockedCompareExchange128. 使用一些jiggery-pokery,您应该能够使用InterlockedCompareExchange128完成此操作。 Store the reference count and pointer in a 2 element __int64 array. 将引用计数和指针存储在2元素__int64数组中。 If reference count is in array[0] and pointer in array[1] the atomic update would look like this: 如果引用计数在数组[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.
    }
}

If an InterlockedCompareExchange128 intrinsic function isn't available for your compiler then you may use the underlying CMPXCHG16B instruction instead, if you don't mind mucking around in assembly language. 如果您的编译器没有InterlockedCompareExchange128内部函数,那么您可以使用底层的CMPXCHG16B指令代替,如果您不介意使用汇编语言。

The solution proposed by RobH doesn't work. RobH提出的解决方案不起作用。 It has the same problem as the original question: when accessing the reference count object, it might already have been deleted. 它与原始问题具有相同的问题:访问引用计数对象时,它可能已被删除。

The only way I see of solving the problem without a global lock (as in boost::atomic_store) or conditional read/write instructions is to somehow delay the destruction of the object (or the shared reference count object if such thing is used). 我在没有全局锁定(如boost :: atomic_store)或条件读/写指令的情况下解决问题的唯一方法是以某种方式延迟对象(或共享引用计数对象,如果使用这样的东西)的破坏。 So zennehoy has a good idea but his method is too unsafe. 所以zennehoy有一个好主意,但他的方法太不安全了。

The way I might do it is by keeping copies of all the pointers in the writer thread so that the writer can control the destruction of the objects: 我可能这样做的方法是保留编写器线程中所有指针的副本,以便编写者可以控制对象的销毁:

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;
       }
    }
};

However there are still requirements for the smart pointer class. 但是,仍然需要智能指针类。 This doesn't work with shared_ptr as the reads and writes are not atomic. 这不适用于shared_ptr,因为读取和写入不是原子的。 It almost works with boost::intrusive_ptr. 它几乎与boost :: intrusive_ptr一起使用。 The assignment on intrusive_ptr is implemented like this (pseudocode): 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);

As far as I understand the only thing missing here is a compiler level memory fence before lhs.ptr = tmp.ptr; 据我所知,这里唯一缺少的是lhs.ptr = tmp.ptr;之前的编译器级别内存栅栏lhs.ptr = tmp.ptr; . With that added, both reading rhs and writing lhs would be thread-safe under strict conditions: 1) x86 or x64 architecture 2) atomic reference counting 3) rhs refcount must not go to zero during the assignment (guaranteed by the Writer code above) 4) only one thread writing to lhs (using CAS you could have several writers). 添加完成后,读取rhs和写入lhs在严格条件下都是线程安全的:1)x86或x64架构2)原子引用计数3) rhs refcount在赋值期间不得为零(由上面的Writer代码保证) 4)只有一个线程写入lhs (使用CAS可以有几个编写器)。

Anyway, you could create your own smart pointer class based on intrusive_ptr with necessary changes. 无论如何,您可以基于intrusive_ptr创建自己的智能指针类,并进行必要的更改。 Definitely easier than re-implementing shared_ptr. 绝对比重新实现shared_ptr更容易。 And besides, if you want performance, intrusive is the way to go. 此外,如果你想要表现,那么侵入性就是你要走的路。

The reason this works much more easily in java is garbage collection. 这在java中更容易工作的原因是垃圾收集。 In C++, you have to manually ensure that a value is not just starting to be used by a different thread when you want to delete it. 在C ++中,您必须手动确保当您想要删除某个值时,该值不会被其他线程开始使用。

A solution I've used in a similar situation is to simply delay the deletion of the value. 我在类似情况下使用的解决方案是简单地延迟删除值。 I create a separate thread that iterates through a list of things to be deleted. 我创建了一个单独的线程,它遍历要删除的事物列表。 When I want to delete something, I add it to this list with a timestamp. 当我想删除某些内容时,我将其添加到带有时间戳的列表中。 The deleting thread waits until some fixed time after this timestamp before actually deleting the value. 删除线程在实际删除该值之前等待此时间戳之后的某个固定时间。 You just have to make sure that the delay is large enough to guarantee that any temporary use of the value has completed. 您只需确保延迟足够大,以确保任何临时使用的值已完成。

100 milliseconds would have been enough in my case, I chose a few seconds to be safe. 在我的情况下100毫秒就足够了,我选择了几秒钟才能安全。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM