簡體   English   中英

在C ++,x86-64中讀寫線程安全的智能指針

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

我開發了一些無鎖數據結構,並出現以下問題。

我有編寫器線程在堆上創建對象並使用引用計數器將它們包裝在智能指針中。 我也有很多讀者線程,可以使用這些對象。 代碼可能如下所示:

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. 增加參考計數器。

我無法原子地執行這兩個操作,因此有時候我的讀者會使用已刪除的對象。

問題是 - 我應該使用什么樣的智能指針來從幾個線程進行讀寫訪問,並且可以進行正確的內存管理? 解決方案應該存在,因為Java程序員甚至不關心這樣的問題,只是依賴於所有對象都是引用,只有在沒有人使用它們時才會被刪除。

對於PowerPC,我發現http://drdobbs.com/184401888 ,看起來不錯,但使用了我們在x86中沒有的Load-Linked和Store-Conditional指令。

據我所知,boost指針僅使用鎖提供此類功能。 我需要無鎖解決方案。

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

使用一些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指令代替,如果您不介意使用匯編語言。

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;之前的編譯器級別內存柵欄lhs.ptr = tmp.ptr; 添加完成后,讀取rhs和寫入lhs在嚴格條件下都是線程安全的:1)x86或x64架構2)原子引用計數3) rhs refcount在賦值期間不得為零(由上面的Writer代碼保證) 4)只有一個線程寫入lhs (使用CAS可以有幾個編寫器)。

無論如何,您可以基於intrusive_ptr創建自己的智能指針類,並進行必要的更改。 絕對比重新實現shared_ptr更容易。 此外,如果你想要表現,那么侵入性就是你要走的路。

這在java中更容易工作的原因是垃圾收集。 在C ++中,您必須手動確保當您想要刪除某個值時,該值不會被其他線程開始使用。

我在類似情況下使用的解決方案是簡單地延遲刪除值。 我創建了一個單獨的線程,它遍歷要刪除的事物列表。 當我想刪除某些內容時,我將其添加到帶有時間戳的列表中。 刪除線程在實際刪除該值之前等待此時間戳之后的某個固定時間。 您只需確保延遲足夠大,以確保任何臨時使用的值已完成。

在我的情況下100毫秒就足夠了,我選擇了幾秒鍾才能安全。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM