使用大于或小于而不是等于的Interlocked.CompareExchange<Int>

29

System.Threading.Interlocked 对象允许以原子操作方式进行加法(减法)和比较。似乎一个能够原子性比较不仅是相等,而且包括大于/小于的 CompareExchange 功能会非常有价值。

一个假设的 Interlocked.GreaterThan 是 IL 的特性还是 CPU 级别的功能?两者都吗?

如果没有其他选项,是否有可能在 C++ 或直接的 IL 代码中创建这样的功能并将该功能暴露给 C#?


你真的想读取一个值,修改它,即使其他线程同时也对它进行了修改,然后再写回去吗?这样做有什么用例呢? - svick
@svick 如果<int>是类似于DateTime时间戳的话,我正在编写逻辑,只应该处理“新”的数据。此外还有Max()和Min()函数。 - makerofthings7
7个回答

62

您可以使用InterlockedCompareExchange来构建其他原子操作,详情请见

public static bool InterlockedExchangeIfGreaterThan(ref int location, int comparison, int newValue)
{
    int initialValue;
    do
    {
        initialValue = location;
        if (initialValue >= comparison) return false;
    }
    while (System.Threading.Interlocked.CompareExchange(ref location, newValue, initialValue) != initialValue);
    return true;
}

这正是我正在寻找的...将现有的内容重新组合成新的结构。 - makerofthings7
1
我不确定是否只有我一个人这样认为,但是这段代码肯定没有提供原子操作。Interlocked API提供了原子操作的功能,但是在这段代码中,你通过串联多个原子语句和while循环等方式绕过了这个功能,这些语句在本质上不再是原子操作。 - Ninjanoel
11
在执行 CompareExchange 操作之前,所有步骤都没有外部可见性,该操作具有原子性。如果您观察大多数处理器,您会发现唯一的原子核心操作是 CompareExchange ,其他所有操作都是由它构建而成的。 - Raymond Chen
如果该函数返回false,则没有进行任何操作。 - Raymond Chen
9
注意:如果使用long代替int,建议将initialValue = location;更改为initialValue = Interlocked.Read(ref location); - vgru
显示剩余5条评论

6

使用这些辅助方法,您不仅可以交换值,还可以检测它是否被替换。

用法如下:

int currentMin = 10; // can be changed from other thread at any moment

int potentialNewMin = 8;
if (InterlockedExtension.AssignIfNewValueSmaller(ref currentMin, potentialNewMin))
{
    Console.WriteLine("New minimum: " + potentialNewMin);
}

以下是方法:

public static class InterlockedExtension
{
    public static bool AssignIfNewValueSmaller(ref int target, int newValue)
    {
        int snapshot;
        bool stillLess;
        do
        {
            snapshot = target;
            stillLess = newValue < snapshot;
        } while (stillLess && Interlocked.CompareExchange(ref target, newValue, snapshot) != snapshot);

        return stillLess;
    }

    public static bool AssignIfNewValueBigger(ref int target, int newValue)
    {
        int snapshot;
        bool stillMore;
        do
        {
            snapshot = target;
            stillMore = newValue > snapshot;
        } while (stillMore && Interlocked.CompareExchange(ref target, newValue, snapshot) != snapshot);

        return stillMore;
    }
}

3
你对这个实现有什么看法:
// this is a Interlocked.ExchangeIfGreaterThan implementation
private static void ExchangeIfGreaterThan(ref long location, long value)
{
    // read
    long current = Interlocked.Read(ref location);
    // compare
    while (current < value)
    {
        // set
        var previous = Interlocked.CompareExchange(ref location, value, current);
        // if another thread has set a greater value, we can break
        // or if previous value is current value, then no other thread has it changed in between
        if (previous == current || previous >= value) // note: most commmon case first
            break;
        // for all other cases, we need another run (read value, compare, set)
        current = Interlocked.Read(ref location);
    }
}

3

更新此前发布的帖子:我们找到了一种更好的方法,通过使用额外的锁对象来进行更大比较。我们编写了许多单元测试以验证锁和Interlocked可以一起使用,但只适用于某些情况。

代码如何工作:Interlocked使用内存屏障使读取或写入是原子操作。同步锁需要使大于比较成为原子操作。因此,现在的规则是,在这个类内部,没有其他操作在没有同步锁的情况下写入值。

我们使用这个类得到的是一个可以非常快速读取的交替值,但写入需要更多的时间。在我们的应用程序中,读取速度大约是写入速度的2-4倍。

以下是代码视图:

请参见此处:http://files.thekieners.com/blogcontent/2012/ExchangeIfGreaterThan2.png

以下是可复制粘贴的代码:

public sealed class InterlockedValue
{
    private long _myValue;
    private readonly object _syncObj = new object();

    public long ReadValue()
    {
        // reading of value (99.9% case in app) will not use lock-object, 
        // since this is too much overhead in our highly multithreaded app.
        return Interlocked.Read(ref _myValue);
    }

    public bool SetValueIfGreaterThan(long value)
    {
        // sync Exchange access to _myValue, since a secure greater-than comparisons is needed
        lock (_syncObj)
        {
            // greather than condition
            if (value > Interlocked.Read(ref  _myValue))
            {
                // now we can set value savely to _myValue.
                Interlocked.Exchange(ref _myValue, value);
                return true;
            }
            return false;
        }
    }
}

谢谢更新。我想知道如果您用自旋锁替换锁,您的方法会快多少。自旋锁是.NET 4.x中许多新的ConcurrentDictionary和其他对象的基础。 - makerofthings7
15
我认为你不需要在lock块内部使用Interlocked,因为你已经通过在_syncObj周围加锁来锁定整个类实例。 - veljkoz
在32位环境下,长整型读取操作不保证是原子性的。这是这篇博客所说的。 - Lamarth
3
@veljkoz:在锁内部不需要使用Interlocked.Read,但是Interlocked.Exchange很重要,因为另一个线程可能会同时调用ReadValue()方法(而在x86上运行时,long不是原子的)。 - vgru

3
这其实并不完全正确,但将并发视为两种形式是有用的:
1.无锁并发
2.基于锁的并发
这并不完全正确,因为软件基于锁的并发最终会在堆栈中使用无锁原子指令来实现(通常在内核中)。但是,所有无锁原子指令最终都会在内存总线上获取硬件锁。所以,实际上,无锁并发和基于锁的并发是相同的。
但是,在用户应用程序的层面上,它们在概念上是两种不同的方式。
基于锁的并发是基于“锁定”对关键代码段的访问的想法。当一个线程“锁定”关键部分时,没有其他线程可以在该相同关键部分内运行代码。这通常通过使用“互斥量”来完成,它与操作系统调度程序进行接口,并导致线程在等待进入锁定关键部分时变得不可运行。另一种方法是使用“自旋锁”,它使线程在循环中旋转,不做任何有用的事情,直到关键部分变为可用为止。
无锁并发是基于使用原子指令(由CPU专门支持)的想法,这些指令被硬件保证以原子方式运行。Interlocked.Increment是无锁并发的一个很好的例子。它只调用特殊的CPU指令,执行原子递增。
无锁并发很难。随着关键部分的长度和复杂性增加,它变得特别困难。任何关键部分的步骤都可以同时由任意数量的线程执行,并且它们可以以非常不同的速度移动。您必须确保尽管如此,系统作为整体的结果仍然正确。对于像递增这样的东西,它可能很简单(cs只是一个指令)。对于更复杂的关键部分,事情可以非常快地变得非常复杂。
基于锁的并发也很难,但不像无锁并发那么难。它允许您创建任意复杂的代码区域,并知道只有1个线程在任何时间执行它。
然而,无锁并发有一个巨大的优势:速度。当使用正确时,它可以比基于锁的并发快几个数量级。自旋循环对于长时间运行的关键部分来说是不好的,因为它们浪费CPU资源什么都不做。互斥量对于小的关键部分可能不好,因为它们引入了很多开销。它们至少涉及一次模式切换,并在最坏的情况下涉及多个上下文切换。
考虑实现托管堆。每次调用“new”时都调用操作系统会很糟糕。它会破坏您的应用程序的性能。但是,使用无锁并发,可以使用交错递增来实现gen 0内存分配(我不确定CLR是否这样做,但如果不是,我会感到惊讶。这可能会带来巨大的节省。
还有其他用途,例如在无锁数据结构中,如持久堆栈和avl树中使用。它们通常使用“cas”(比较和交换)。
然而,基于锁的并发和无锁并发真正等效的原因是由于每个实现
自旋锁通常在其循环条件中使用原子指令(通常是cas)。互斥锁需要在其实现中使用自旋锁或内部内核结构的原子更新。
这些原子指令又是使用硬件锁来实现的。
无论哪种情况,它们都有各自的优缺点,通常集中于性能与复杂性之间的取舍。互斥锁可以比无锁代码更快或更慢。无锁代码可以比互斥锁更复杂或更简单。使用适当的机制取决于具体情况。
现在回答你的问题:
如果一个方法使用原子比较交换如果小于,那么对调用者来说就意味着它没有使用锁。你无法像增量或比较交换一样使用单个指令实现它。您可以使用循环中的交错比较交换来模拟它(计算小于)。也可以使用互斥锁(但这将暗示存在锁定,因此在名称中使用“交错”会是误导性的)。是否应该构建“通过cas模拟的交错锁定版本”? 这取决于具体情况。如果代码被频繁调用并且线程争用非常少,则答案是肯定的。否则,您可能会将具有适度高常数因子的O(1)操作转换为无限(或非常长的)循环,此时最好使用互斥锁。
大多数时间不值得这样做。

FYI,在许多平台上,应用程序可以请求一定量的线程本地数据,这些数据可以被非常快速地访问;.NET不向用户代码公开此功能,但据我所知,其内存分配器的某些版本将gen0堆细分为不同的块,然后将这些块分配给不同的核心;每个核心都有自己的下一个分配指针,允许它从其块中分配内存,而无需与其他任何内容同步。 - supercat
此外,我认为包含读取-修改-比较交换循环的互锁方法没有任何问题。这就是Interlocked.Increment(ref long)的一些实现所做的。只要从旧值计算新值的计算速度快,代码在循环中花费任何显著时间的危险性并不真正存在;除非其他线程有意恶意,否则最坏情况下的失败尝试次数将等于核心数。 - supercat

1

所有交错操作在硬件中都有直接支持。

交错操作和原子数据类型是不同的东西。原子类型是一个库级别的特性。在某些平台上和对于某些数据类型,原子操作是使用交错指令来实现的。在这种情况下,它们非常有效。

在其他情况下,当平台根本没有交错操作或者对于某些特定的数据类型不可用时,库会使用适当的同步(crit_sect、mutex等)来实现这些操作。

我不确定是否真的需要 Interlocked.GreaterThan。否则它可能已经被实现了。如果您知道一个好的例子,在哪里它可能有用,我相信每个人都会很高兴听到这个消息。


1
我正在实现一个轻量级(复杂数据处理/CEP)类分析引擎。我读取的数据具有无序的日期时间戳。根据比较结果,也许我可以使用CompareExchangeGreaterThan(ref int, int, int)来加快速度,而不需要任何自旋锁。 - makerofthings7
PS - 是的,我知道DateTime是一个Double类型,但我只需要“1970年以来的秒数”,这样我就可以将我的数据适配到更小的内存分配,比如int类型。 - makerofthings7
我明白了。这可能会很有用。另一方面,由于它是硬件相关的,你知道,Intel很少可能将其添加到其平台中。 Intel需要明显的性能提升来证明这个特性值得投资。硬件的更改并不便宜。 - Kirill Kobelev

0

大于/小于等于已经是原子操作了。但这并不能解决你的应用程序在并发时的安全行为。

将它们作为Interlocked家族的一部分没有意义,所以问题是:你实际上想要实现什么?


我想在原子比较后存储更大的值。 - makerofthings7

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接