作为无锁代码的第一次动手冒险(我直到现在才读到它),我想尝试为IDisposable构建一个无锁的引用计数包装器类。
这是实际的无锁嵌套类:
private sealed class Wrapper
{
public T WrappedObject { get; private set; }
private int refCount;
public Wrapper(T objectToWrap)
{
WrappedObject = objectToWrap;
refCount = 1;
}
public void RegisterShare()
{
Interlocked.Increment(ref refCount);
}
public bool TryRegisterShare()
{
int prevValue;
do {
prevValue = refCount;
if (prevValue == 0)
return false;
} while (prevValue != Interlocked.CompareExchange(ref refCount, prevValue + 1, prevValue));
return true;
}
public void UnregisterShare()
{
if (Interlocked.Decrement(ref refCount) <= 0)
WrappedObject.Dispose();
}
}
它是一个私有嵌套类,因此我可以确保仅为以下目的调用方法:
RegisterShare
用于复制强引用UnregisterShare
用于发布强引用TryRegisterShare
用于将弱引用推广为强引用我相信我有了基本的想法,我不确定这是否真的是线程安全的。我想到的一个问题:在TryRegisterShare
中,prevValue
的第一个赋值是否保证大于零,除非所有强引用都被释放?我需要一些围栏或挥发物吗?
我不相信处理参考文献分享的外部课程对此很重要,但如果有兴趣的话,你可以在这里找到它:https://codepaste.net/zs7nbh
这里修改过的代码考虑了@PeterCordes所说的内容。
private sealed class Wrapper
{
public T WrappedObject { get; private set; }
private int refCount;
public Wrapper(T objectToWrap)
{
WrappedObject = objectToWrap;
refCount = 1;
}
public void RegisterShare()
{
Interlocked.Increment(ref refCount);
}
public bool TryRegisterShare()
{
return Interlocked.Increment(ref refCount) > 0;
}
public void UnregisterShare()
{
if (Interlocked.Decrement(ref refCount) == 0
&& Interlocked.CompareExchange(ref refCount, int.MinValue, 0) == 0)
{
WrappedObject.Dispose();
}
}
}
答案 0 :(得分:1)
您正在实施与C++11's std::shared_ptr
类似的内容,因此您可以查看它的实现以获取灵感。 (此类类似于后端控制块,用于分隔共享同一指针的shared_ptr
个对象。)
如果两个线程都运行UnregisterShare
并将引用计数降低到零以下,那么它们都会尝试.Dispose()
。这是一个错误,类似于双重免费或双重解锁。您可以通过将代码更改为==
而不是<=
来检查它或将其打包,因此只有一个线程运行.Dispose()
。 <=
看起来像是两个世界中最糟糕的:可能很难识别的不良行为。
TryRegisterShare
中的,除非所有强引用都被释放,否则prevvalue的第一个赋值保证大于零?
除非在参考时未能调用RegisterShare,或者发布错误的错误,否则我认为是这样。在那里使用if(prevValue <= 0) return false
是明智的,以确保你在双重释放的情况下拯救,因为某些事情导致引用数量为负。
cmpxchg循环看起来并不理想,但是如果你无条件地增加并且只是检查你是否必须从0开始,这可能会欺骗其他线程。 (例如,这一系列事件:
我还没看过(Linux / gcc实现)C ++ 11 weak_ptr.lock()
如何实现对shared_ptr
的推广(我现在很好奇!)。
我想知道他们是否在循环中使用cmpxchg(在asm中),或者他们是否会做某种避免这种情况的舞蹈。例如也许Unshare,在检测到refcount == 0时,可以使用cmpxchg循环在调用Dispose之前将refcount从零修改为-2 ^ 31 。 (如果它在该循环中检测到refcount&gt; =,它会停止尝试杀死它。)然后我认为只要看到Interlocked.Increment(ref refCount) >= 1
,TryShare就会成功,因为这意味着任何正在运行的UnShare的cmpxchg还没有成功,并且没有成功。
对于某些用例,TryShare可能不希望在这种情况下成功。你可以让它调用Unshare再次递减计数,如果它的增量发现旧值为零。
所以我认为在某个地方需要有一个cmpxchg循环,但是如果我的逻辑是正确的,你可以放弃将它放在UnShare的refcount-drops-zero-zero路径中,而不是在可能更热的{{{ 1}}。
TryRegisterShare
看起来很安全,如果你可以绝对保证引用数量不是事先为零。在已经有ref的线程中应该是这种情况。对于错误检查,您可以检查增量的返回值,以捕获您不小心复活死对象的情况(即,另一个线程即将(或可能已经)已处置。
在C ++中,如果多个线程正在读取和写入相同的RegisterShare
,则可能会违反此假设。但是,这需要shared_ptr
才能安全,that won't compile因为std::atomic<std::shared_ptr<int>> foo;
并非易于构建。
因此,C ++保护自己免受对参考包装器对象的未锁定并发访问(通过声明未定义行为);你也应该。否则,另一个引用此线程正在复制的shared_ptr
对象的线程可能会在其上调用shared_ptr
,因此在此线程读取指向控制块的指针后,可能会减少控制块中的refcount ,但之前此线程会增加控制块引用计数。
我刚注意到标题问题是关于需要额外的栅栏或挥发物:
UnShare中的减量需要在.reset()
执行任何操作之前全局可见,并且需要为release operation以确保加载/存储到共享对象之前在全局可见之前变为全局可见引用。
您的代码已经实现了这一点,因为.Dispose()
在任何体系结构(不仅仅是Windows)上都具有.NET中的顺序一致性语义,因此没有任何内容可以在任何方向上重新排序。
AlexRP's blog post about the .NET memory model 详细说明了相关内容(联锁,Interlocked.anything
/ Thread.VolatileRead
,Write
/ Volatile.Read
,宽度为Write
的类型上的简单加载或存储是自动原子的(但不是同步的)。)有趣的事实:在Microsoft的x86实现中,IntPtr.Size
和写入是由MFENCE指令在任何一方包围,但语言标准只需要获取或释放语义(x86在没有障碍的情况下免费提供)。 Mono并没有为这些功能使用障碍,但MS无法删除它们,直到每个人的错误代码停止依赖于该实现细节。
除了减量之外,我认为其他一切只需要原子性来进行重新计数,而不是任何其他操作的同步。引用计数不会从多个线程同步对共享对象的访问,因此TryShare / UnShare对不会形成关键部分(需要获取/释放语义)。从构造函数到导致Thread.VolatileRead
的最终UnShare,此类在同步时是不干涉的。在C ++中,我认为您可以使用.Dispose()
作为RegisterShare增量和TryShare cmpxchg。在一些弱有序的架构上,比如ARM,这将需要更少的障碍。 (这在C#中没有选择:你只有顺序一致性的互锁操作,这需要ARM的完全障碍。但是,x86上没有额外的成本,因为memory_order_relaxed
读取了x86上的修改写入指令已经是完整的内存障碍,如MFENCE(lock
))