我正在努力理解LFRC算法,但我一直找到的例子都是在我假设它们有效的情况下工作,但我无法理解为什么。我关注的是复制和删除。特别是有三种算法都遵循相同的一般模式(伪代码):
Copy {
shared = source->shared;
if (shared != null) {
Atomically_Increment_Counter(shared);
}
}
Delete {
if (shared != null) {
oldcounter = Atomically_Decrement_Counter(shared);
if (oldcounter == value signifying all references released) {
Deallocate(shared);
}
}
}
首先,这篇文章是2004年David L. Detlefs所写的论文《无锁引用计数》,其中的图2、第8页(为了格式而编辑):
void LFRCCopy(SNode **v, SNode *w) {
Snode *oldv = *v;
if (w != null)
add_to_rc(w,1);
*v = w;
LFRCDestroy(oldv);
}
void LFRCDestroy(SNode *v) {
if (v != null && add_to_rc(v, -1)==1) {
LFRCDestroy(v->L);
LFRCDestroy(v->R);
delete v;
}
}
其中add_to_rc
是原子增量和加载。第二个来自于opencv4中Mat
实现(编辑以缩短长度):
// Line 405
Mat::Mat(const Mat& m)
: flags(m.flags), dims(m.dims), rows(m.rows), cols(m.cols), data(m.data),
datastart(m.datastart), dataend(m.dataend), datalimit(m.datalimit), allocator(m.allocator),
u(m.u), size(&rows), step(0)
{
if( u )
CV_XADD(&u->refcount, 1);
...
}
// Line 551
void Mat::release()
{
if( u && CV_XADD(&u->refcount, -1) == 1 )
deallocate();
u = NULL;
...
}
其中CV_XADD
是原子增量和加载。第三个来自最新版本的GCC(10.2)中随附的libstdc++中的std::shared_ptr
实现(此实现非常复杂,我已经为了简洁性进行了编辑):
class _Sp_counted_base {
void _M_add_ref_copy()
{ __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
// Line 161 for full implementation
void _M_release() noexcept
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
_M_dispose();
if (_Mutex_base<_Lp>::_S_need_barriers)
__atomic_thread_fence (__ATOMIC_ACQ_REL);
// editors note: weak pointer handling removed for brevity
_M_destroy();
}
}
}
class __shared_count {
__shared_count(const __shared_count& __r) noexcept
: _M_pi(__r._M_pi)
{
if (_M_pi != nullptr)
_M_pi->_M_add_ref_copy();
}
~__shared_count() noexcept
{
if (_M_pi != nullptr)
_M_pi->_M_release();
}
_Sp_counted_base* _M_pi;
}
class __shared_ptr {
__shared_ptr(const __shared_ptr&) noexcept = default;
~__shared_ptr() = default;
element_type* _M_ptr; // Contained pointer.
__shared_count _M_refcount; // Reference counter.
}
关于最后一个问题的一些解释,因为它有点间接:
- 复制:
__shared_ptr
默认的复制构造函数复制了_M_refcount
,其复制构造函数共享相同的_Sp_counted_base
作为原始对象,并原子性地增加引用计数。 - 删除:
__shared_ptr
默认的析构函数销毁_M_refcount
,其析构函数调用_M_release
在_Sp_counted_base
上,该函数原子性地减少并可能释放引用计数。
总之,这归结为一个问题,这是我无法理解为什么这些工作(即使那篇旧的Dr. Dobbs文章和相关的SO问题[我觉得答案可能在这里但我看不到]对我来说提出了更多的问题):
在复制操作中,有什么可以防止另一个线程在最后一个引用计数上执行删除操作(可能通过共享对象的另一个视图),然后在复制操作开始和原子计数器增量开始之间释放对象,因此,从我(误)理解的情况来看,复制将增加一个已分配的计数器并在各处崩溃或转储核心?
也就是说,参考上面的opencv实现(因为检查它实际上启动了我整个小型研究项目):
Mat::Mat(const Mat& m)
: flags(m.flags), dims(m.dims), rows(m.rows), cols(m.cols), data(m.data),
datastart(m.datastart), dataend(m.dataend), datalimit(m.datalimit), allocator(m.allocator),
u(m.u), // <--- START OF DANGER ZONE
size(&rows), step(0)
{
if( u )
// <--- ALMOST AT END OF DANGER ZONE
CV_XADD(&u->refcount, 1);
...
}
void Mat::release()
{
if( u && CV_XADD(&u->refcount, -1) == 1 )
deallocate();
u = NULL;
...
}
另一个线程如何防止在复制构造函数中通过 m
在标记为“危险区”的位置释放最后一个引用,从而使 u
非空但指向已释放的垃圾内存?
我错过了什么?我觉得这可能很明显,也许是因为我已经醒太久了。特别是,libstdc++ 实现似乎也遵循这种模式,给它带来了很多信誉;只是我无法理解细节。
shared_ptr
对象(在你的情况中是x
)。这意味着你不能在一个线程中"复制"它并在另一个线程中"销毁"它。 - undefinedshared_ptr
,那么另一个线程要么有自己的shared_ptr
并且对引用计数有一个“股份”,即其>=2 或者其他同步机制确保了“借用”。如果是借用的话,首选方法是传递原始指针(shared_ptr::get()
)来表示它具有对存储对象的引用,但不直接参与其引用计数。 - undefinedwas=Atomically_Decrement_Counter(shared);
,然后下一行变成if(was==INIT_VALUE)
,其中INIT_VALUE
是在构造过程中共享变量的初始值,并且如果存在特殊生命周期,可能不是1。was
必须是原子递减期间共享变量被替换的值。 - undefined