简体   繁体   English

可以在这个多线程循环实现中正确使用 Interlocked CompareExchange 吗?

[英]Can Interlocked CompareExchange be used correctly in this multithreaded round-robin implementation?

I need to round-robin some calls between N different connections because of some rate limits in a multithreaded context.由于多线程上下文中的某些速率限制,我需要在 N 个不同连接之间循环一些调用。 I've decided to implement this functionality using a list and a "counter," which is supposed to "jump by one" between instances on each call.我决定使用一个列表和一个“计数器”来实现这个功能,它应该在每次调用的实例之间“跳一个”。

I'll illustrate this concept with a minimal example (using a class called A to stand in for the connections)我将用一个最小的例子来说明这个概念(使用一个名为 A 的 class 来代替连接)

class A
{
    public A()
    {
        var newIndex = Interlocked.Increment(ref index);
        ID = newIndex.ToString();
    }
    private static int index;
    public string ID;
}

static int crt = 0;
static List<A> Items = Enumerable.Range(1, 15).Select(i => new A()).ToList();
static int itemsCount = Items.Count;

static A GetInstance()
{            
    var newIndex = Interlocked.Increment(ref crt);
    var instance = Items[newIndex % itemsCount];
    //Console.WriteLine($"{DateTime.Now.Ticks}, {Guid.NewGuid()}, Got instance: {instance.ID}");
    return instance;
}

static void Test()
{
    var sw = Stopwatch.StartNew();

    var tasks = Enumerable.Range(1, 1000000).Select(i => Task.Run(GetInstance)).ToArray();
    Task.WaitAll(tasks);
}

This works as expected in that it ensures that calls are round-robin-ed between the connections.这按预期工作,因为它确保调用在连接之间是循环的。 I will probably stick to this implementation in the "real" code (with a long instead of an int for the counter)我可能会在“真实”代码中坚持这个实现(计数器使用 long 而不是 int)

However, even if it is unlikely to reach int.MaxValue in my use case, I wondered if there is a way to "safely overflow" the counter.但是,即使在我的用例中不太可能达到 int.MaxValue ,我想知道是否有办法“安全地溢出”计数器。

I know that "%" in C# is "Remainder" rather than "Modulus," which would mean that some?: gymnastics would be required to always return positives, which I want to avoid.我知道 C# 中的“%”是“余数”而不是“模数”,这意味着一些?:体操需要始终返回正数,我想避免这种情况。

So what I wanted to cume up with is instead something like:所以我想提出的是:

static A GetInstance()
{            
    var newIndex = Interlocked.Increment(ref crt);
    Interlocked.CompareExchange(ref crt, 0, itemsCount); //?? the return value is the original value, how to know if it succeeded
    var instance = Items[newIndex];
    //Console.WriteLine($"{DateTime.Now.Ticks}, {Guid.NewGuid()}, Got instance: {instance.ID}");
    return instance;
}

What I am expecting is that Interlocked.CompareExchange(ref crt, 0, itemsCount) would be "won" by only one thread, setting the counter back to 0 once it reaches the number of connections available.我期望的是Interlocked.CompareExchange(ref crt, 0, itemsCount)将仅由一个线程“赢得”,一旦达到可用连接数,将计数器设置回 0。 However, I don't know how to use this in this context.但是,我不知道如何在这种情况下使用它。

Can CompareExchange or another mechanism in Interlocked be used here?可以在这里使用 CompareExchange 或 Interlocked 中的其他机制吗?

You could probably:你可能可以:

static int crt = -1;
static readonly IReadOnlyList<A> Items = Enumerable.Range(1, 15).Select(i => new A()).ToList();
static readonly int itemsCount = Items.Count;
static readonly int maxItemCount = itemsCount * 100;

static A GetInstance()
{
    int newIndex;

    while (true)
    {
        newIndex = Interlocked.Increment(ref crt);

        if (newIndex >= itemsCount)
        {
            while (newIndex >= itemsCount && Interlocked.CompareExchange(ref crt, -1, newIndex) != newIndex)
            {
                // There is an implicit memory barrier caused by the Interlockd.CompareExchange around the
                // next line
                // See for example https://afana.me/archive/2015/07/10/memory-barriers-in-dot-net.aspx/
                // A full memory barrier is the strongest and interesting one. At least all of the following generate a full memory barrier implicitly:
                // Interlocked class mehods
                newIndex = crt;
            }

            continue;
        }

        break;
    }

    var instance = Items[newIndex % itemsCount];
    //Console.WriteLine($"{DateTime.Now.Ticks}, {Guid.NewGuid()}, Got instance: {instance.ID}");
    return instance;
}

But I have to say the truth... I'm not sure if it is correct (it should be), and explaining it is hard, and if anyone touches it in any way it will break.但是我不得不说实话……我不确定它是否正确(应该是),并且很难解释它,如果任何人以任何方式触摸它都会破坏它。

The basic idea is to have a "low" ceiling for crt (we don't want to overflow, it would break everything... so we want to keep veeeeeery far from int.MaxValue , or you could use uint ).基本想法是为crt设置一个“低”上限(我们不想溢出,它会破坏一切......所以我们希望保持 veeeeeery 远离int.MaxValue ,或者您可以使用uint )。

The maximum possible value is:最大可能值为:

maxItemCount = (int.MaxValue - MaximumNumberOfThreads) / itemsCount * itemsCount;

The / itemsCount * itemsCount is because we want the rounds to be equally distributed. / itemsCount * itemsCount是因为我们希望轮次平均分配。 In the example I give I use a probably much lower number ( itemsCount * 100 ) because lowering this ceiling will only cause the reset more often, but the reset isn't so much slow that it is truly important (it depends on what you are doing on the threads. If they are very small threads that only use cpu then the reset is slow, but if not then it isn't).在我给出的示例中,我使用了一个可能低得多的数字( itemsCount * 100 ),因为降低这个上限只会更频繁地导致重置,但重置并没有那么慢以至于它真的很重要(这取决于你是什么在线程上做。如果它们是仅使用 cpu 的非常小的线程,则重置很慢,但如果不是,则不是)。

Then when we overflow this ceiling we try to move it back to -1 (our starting point).然后当我们溢出这个上限时,我们尝试将其移回-1 (我们的起点)。 We know that at the same time other bad bad threads could Interlocked.Increment it and create a race on this reset.我们知道同时其他糟糕的坏线程可能会Interlocked.Increment 。增加它并在此重置时创建竞争。 Thanks to the Interlocked.CompareExchange only one thread can successfully reset the counter, but the other racing threads will immediately see this and break from their attempts.多亏了Interlocked.CompareExchange ,只有一个线程可以成功重置计数器,但其他竞赛线程会立即看到这一点并中断他们的尝试。

Mmmh... The if can be rewritten as:嗯... if可以重写为:

if (newIndex >= itemsCount)
{
    int newIndex2;
    while (newIndex >= itemsCount && (newIndex2 = Interlocked.CompareExchange(ref crt, 0, newIndex)) != newIndex)
    {
        // If the Interlocked.CompareExchange is successfull, the while will end and so we won't be here,
        // if it fails, newIndex2 is the current value of crt
        newIndex = newIndex2;
    }

    continue;
}

No, the Interlocked class offers no mechanism that would allow you to restore an Int32 value back to zero in case it overflows.不, Interlocked的 class 没有提供任何机制来允许您将Int32值恢复为零以防它溢出。 The reason is that it is possible for two threads to invoke concurrently the var newIndex = Interlocked.Increment(ref crt);原因是两个线程可以同时调用var newIndex = Interlocked.Increment(ref crt); statement, in which case both with overflow the counter, and then none will succeed in updating the value back to zero.语句,在这种情况下,两者都会溢出计数器,然后 none 将成功将值更新回零。 This functionality is just beyond the capabilities of the Interlocked class.此功能刚刚超出Interlocked class 的功能。 To make such complex operations atomic you'll need to use some other synchronization mechanism, like a lock .要使这种复杂的操作原子化,您需要使用其他一些同步机制,例如lock


Update: xanatos's answer proves that the above statement is wrong.更新: xanatos 的回答证明上述说法是错误的。 It is also proven wrong by the answers of this 9-year old question. 这个9 年前的问题的答案也证明了它是错误的。 Below are two implementation of an InterlockedIncrementRoundRobin method.下面是InterlockedIncrementRoundRobin方法的两个实现。 The first is a simplified version of this answer, by Alex Sorokoletov:第一个是这个答案的简化版本,作者 Alex Sorokoletov:

public static int InterlockedRoundRobinIncrement(ref int location, int modulo)
{
    // Arguments validation omitted (the modulo should be a positive number)
    uint current = unchecked((uint)Interlocked.Increment(ref location));
    return (int)(current % modulo);
}

This implementation is very efficient, but it has the drawback that the backing int value is not directly usable, since it circles through the whole range of the Int32 type (including negative values).这种实现非常有效,但它的缺点是不能直接使用支持的int值,因为它循环了Int32类型的整个范围(包括负值)。 The usable information comes by the return value of the method itself, which is guaranteed to be in the range [0..modulo] .可用信息来自方法本身的返回值,保证在[0..modulo]范围内。 If you want to read the current value without incrementing it, you would need another similar method that does the same int -> uint -> int conversion:如果您想读取当前值而不增加它,您将需要另一个类似的方法来执行相同的int -> uint -> int转换:

public static int InterlockedRoundRobinRead(ref int location, int modulo)
{
    uint current = unchecked((uint)Volatile.Read(ref location));
    return (int)(current % modulo);
}

It also has the drawback that once every 4,294,967,296 increments, and unless the modulo is a power of 2, it returns a 0 value prematurely, before having reached the modulo - 1 value.它还有一个缺点,即每 4,294,967,296 增量一次,除非modulo是 2 的幂,否则它会在达到modulo - 1值之前过早返回0值。 In other words the rollover logic is technically flawed.换句话说,翻转逻辑在技术上存在缺陷。 This may or may not be a big issue, depending on the application.这可能是一个大问题,也可能不是一个大问题,具体取决于应用程序。

The second implementation is a modified version of xanatos's algorithm :第二种实现是 xanatos算法的修改版本:

public static int InterlockedRoundRobinIncrement(ref int location, int modulo)
{
    // Arguments validation omitted (the modulo should be a positive number)
    while (true)
    {
        int current = Interlocked.Increment(ref location);
        if (current >= 0 && current < modulo) return current;

        // Overflow. Try to zero the number.
        while (true)
        {
            int current2 = Interlocked.CompareExchange(ref location, 0, current);
            if (current2 == current) return 0; // Success
            current = current2;
            if (current >= 0 && current < modulo)
            {
                break; // Another thread zeroed the number. Retry increment.
            }
        }
    }
}

This is slightly less efficient (especially for small modulo values), because once in a while an Interlocked.Increment operation results to an out-of-range value, and the value is rejected and the operation repeated.这效率稍低(尤其是对于小的modulo值),因为有时Interlocked.Increment操作会导致超出范围的值,并且该值被拒绝并重复操作。 It does have the advantage though that the backing int value remains in the [0..modulo] range, except for some very brief time spans, during some of this method's invocations.尽管在此方法的某些调用期间,除了一些非常短暂的时间跨度外,它确实具有支持int值保持在[0..modulo]范围内的优势。

An alternative to using CompareExchange would be to simply let the values overflow.使用 CompareExchange 的替代方法是简单地让值溢出。

I have tested this and could not prove it wrong (so far), but of course that does not mean that it isn't.我已经对此进行了测试并且无法证明它是错误的(到目前为止),但这并不意味着它不是。

 //this incurs some cost, but "should" ensure that the int range
 // is mapped to the unit range (int.MinValue is mapped to 0 in the uint range)
 static ulong toPositive(int i) => (uint)1 + long.MaxValue + (uint)i;

 static A GetInstance()
 {
    //this seems to overflow safely without unchecked
    var newCounter = Interlocked.Increment(ref crt);
    //convert the counter to a list index, that is map the unsigned value
    //to a signed range and get the value modulus the itemCount value
    var newIndex = (int)(toPositive(newCounter) % (ulong)itemsCount);
    var instance = Items[newIndex];
    //Console.WriteLine($"{DateTime.Now.Ticks}, Got instance: {instance.ID}");
    return instance;
 }

PS: Another part of the xy problem part of my question: At a friend's suggestion I am currently investigating using a LinkedList or something similar (with locks) to achieve the same purpose. PS:我的问题的另一部分 xy 问题部分:在朋友的建议下,我目前正在调查使用 LinkedList 或类似的东西(带锁)来实现相同的目的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM