我在非常流行的LazyCache库中发现了一些多线程代码,它们使用int[]字段作为细粒度锁定机制,目的是防止同时调用具有相同key作为参数的方法.我高度怀疑这个代码的正确性,因为在离开保护区时没有使用InterlockedVolatile操作.以下是代码的重要部分:

private readonly int[] keyLocks;

public virtual T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory,
     MemoryCacheEntryOptions policy)
{
    /* Do stuff */

    object cacheItem;
    // acquire lock per key
    uint hash = (uint)key.GetHashCode() % (uint)keyLocks.Length;
    while (Interlocked.CompareExchange(ref keyLocks[hash], 1, 0) == 1) Thread.Yield();
    try
    {
        cacheItem = CacheProvider.GetOrCreate<object>(key, CacheFactory);
    }
    finally
    {
        keyLocks[hash] = 0;
    }

    /* Do more stuff */
}

Link to the source code.

受保护的方法调用是CacheProvider.GetOrCreate<object>(key, CacheFactory).它应该一次由一个线程调用,用于相同的key.为了进入保护区域,有while个循环,它使用Interlocked.CompareExchangekeyLocks数组的值从0改变为1.到目前一切尚好.让我担心的是离开保护区的那条线:keyLocks[hash] = 0;.因为那里没有障碍,我的理解是C#编译器和.NET抖动可以自由地向任何方向移动指令,跨过这条线.因此,CacheProvider.GetOrCreate方法中的指令可以移动到keyLocks[hash] = 0;之后.

My question is:根据规范,上面的代码真的确保CacheProvider.GetOrCreate不会用相同的密钥并发调用吗?这部法典兑现了互斥的promise 吗?还是代码有问题?

Context:相关代码被添加到库中的拉取请求中:Optimize cache to lock per key.

推荐答案

在我看来是错误的;keyLocks[hash] = 0;不是release store,因此Do stuff的部分可以重新排序到临界区之外,可能只有在获得锁后才可能对另一个线程可见.

(可能正在读取已修改的数据,或者更有可能的情况是,存储出现得较晚,并且会踩到下一个线程中的存储,或者它的加载看不到存储.)

它很可能会在x86、where all asm stores have "release" semantics上编译以纠正ASM,因此只有编译时重新排序才会 destruct 东西,但在ARM/AArch64或其他排序较弱的主流ISA上不会.因此,在x86上进行测试并不能发现这个错误,除非您确实得到了编译时的重新排序.(它仍然是坏的,臭虫只是处于Hibernate 状态.)

https://preshing.com/2012relaxed9/this-is-why-they-call-it-a-weakly-ordered-cpu/在C++中演示了一个使用relaxed而不是acquire/release的自旋锁,并且在ARM上的实践中它被打破了.这个例子就像这样,除了这里的CAS类似于C++memory_order_seq_cst,所以临界区的顶部足够强大.但这还不够;更强的获取锁的顺序并不能使您免于太弱的解锁.


一个基本的自旋锁需要一个收购RMW来获得独家所有权,需要一个释放store 来解锁,因此才有了这个名字.这足以将Do stuff保持在该方向的临界区内.

In C#, a release store can be doneVolatile.Write一起使用,或通过赋值给volatile对象.我的理解是,它们等同于C++foo.store(val, std::memory_order_release).

相关的x86 ASM示例和自旋锁讨论:

  • Spinlock with XCHG unlocking(解锁not需要是原子RMW,不需要像Interlocked.Exchange那样 Solidity ,但需要release)
  • Locks around memory manipulation via inline assembly手工编写的x86 ASM,讨论了try 首先获取锁,然后旋转只读与从只读访问开始,只读访问针对争用情况进行了优化.这里的天真的锁继续垃圾邮件原子CAStry ,即使我们还没有看到任何证据表明它可能是可用的.它使用Thread.Yield()而不是SpinWait.SpinOnce(),如果线程比核心多,并且临界区往往需要很长时间才能解锁,这可能是很好的 Select .

Csharp相关问答推荐

SortedSet.IsSubsetOf未按预期工作

从C#网站调用时找不到存储过程

我们应该如何在IHostedService中使用按请求的GbContent实例?

我如何才能获得被嘲笑班级的私有成员?

为什么Blazor值在更改后没有立即呈现?

C++/C#HostFXR通过std::tuple传递参数

在实时数据库中匹配两个玩家的问题

从Blob存储中提取tar.gz文件并将提取结果上载到另一个Blob存储

从.Net 6 DLL注册和检索COM对象(Typelib导出:类型库未注册.(异常来自HRESULT:0x80131165))

C#中Java算法的类似功能

从另一个不同 struct 的数组创建Newtonsoft.Json.Linq.J数组

.NET:从XPath定位原始XML文档中的 node

C#自定义验证属性未触发IsValid方法

在implementationFactory中避免循环依赖

如何更改新创建的实例的变量?

WinUI 3中DoubleCollection崩溃应用程序类型的依赖属性

是否可以从IQueryable T中获取一个IdentyEntry T>

如何在绑定到数据库的datagridview中向上或向下移动行

SignalR跨域

使用ITfoxtec.Identity.Saml2解析相同键多值SAML 2声明