7

If many threads are calling GetNextNumber simultaneously with the following code, GetNextNumber will return 1 more times than any other numbers.

private class RoundRobbinNumber
{
    private int _maxNumbers = 10;
    private int _lastNumber;

    private RoundRobbinNumber(int maxNumbers)
    {
        _maxNumbers = maxNumbers;
    }

    public int GetNextNumber()
    {
        int nextNumber = Interlocked.Increment(ref _lastNumber);
        if (_lastNumber > _maxNumbers)
        {
            Interlocked.CompareExchange(ref _lastNumber, 1, _maxNumbers);
            nextNumber = 1;
        }
        return nextNumber;
    }
}

Is there a way to reset the _lastNumber back to one, and reliably return an incremented number for each thread calling GetNextNumber(), without having to use a lock?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Eric Dahlvang
  • 8,252
  • 4
  • 29
  • 50

4 Answers4

7

Andrey's answer without conditional statements:

using System;
namespace Utils
{
    public class RoundRobinCounter
    {
        private int _max;
        private int _currentNumber = 0;

        public RoundRobinCounter(int max)
        {
            _max = max;
        }

        public int GetNext()
        {
            uint nextNumber = unchecked((uint)System.Threading.Interlocked.Increment(ref _currentNumber));
            int result = (int)(nextNumber % _max);
            return result;
        }
    }
}

And here is a .net fiddle running this code.

Alex Sorokoletov
  • 3,102
  • 2
  • 30
  • 52
5

The trick is to do the operation in a loop until it is successful. I provide a general template for this approach in my answer here.

public int GetNextNumber()
{
  int initial, computed;
  do
  {
    initial = _lastNumber;
    computed = initial + 1;
    computed = computed > _maxNumbers ? computed = 1 : computed;
  } 
  while (Interlocked.CompareExchange(ref _lastNumber, computed, initial) != initial);
  return computed;
}
Community
  • 1
  • 1
Brian Gideon
  • 47,849
  • 13
  • 107
  • 150
  • hmmm...that same method is used in the msdn example here: http://msdn.microsoft.com/en-us/library/801kt583.aspx ...But, there is a comment that states: "The Add method, introduced in version 2.0 of the .NET Framework, provides a more convenient way to accumulate thread-safe running totals for integers." So, I just wanted to be sure I'm not missing something. Thank you for your input. – Eric Dahlvang Apr 05 '12 at 19:16
  • @EricDahlvang - You're not asking for a running total... That makes all the difference. – H H Apr 05 '12 at 19:18
  • 1
    This is a clever solution, but I would prefer a lock() any time. – H H Apr 05 '12 at 19:19
  • @HenkHolterman: Yeah, me too, but since the OP asked for "least contention" solution I figured it was worth posting. – Brian Gideon Apr 05 '12 at 19:26
  • @EricDahlvang: The problem is that `Interlocked.Add` does not do the rollover logic. You'd still have to a [CAS](http://en.wikipedia.org/wiki/Compare-and-swap) in a loop to get the value set correctly. – Brian Gideon Apr 05 '12 at 19:28
3

Not sure if if helps anyone but this could be even simpler:

class RoundRobinNumber
{
    private int _maxNumbers = 10;
    private int _lastNumber = 0;

    public RoundRobinNumber(int maxNumbers)
    {
        _maxNumbers = maxNumbers;
    }

    public int GetNextNumber()
    {
        int nextNumber = Interlocked.Increment(ref _lastNumber);

        int result = nextNumber % _maxNumbers;  

        return result >= 0 ? result : -result;
    }
}
Andrey Ilnitsky
  • 456
  • 4
  • 4
1

Usually round-robin is used to select an item from a collection. Based on Alex's answer I made a RoundRobinCollection variant.

public class RoundRobinCollection<T>
{
    private readonly ReadOnlyCollection<T> _collection;
    private int _currentNumber = -1;

    public RoundRobinCollection(IEnumerable<T> enumerable)
    {
        _collection = new List<T>(enumerable).AsReadOnly();

        if (!_collection.Any())
        {
            throw new InvalidOperationException("Cannot use empty collection for RoundRobinCollection.");
        }
    }

    public T GetNext()
    {
        var index = GetNextIndex();
        return _collection[index];
    }

    private int GetNextIndex()
    {
        // This increments the currentNumber in a Thread-safe way, and deals with exceeding int.MaxValue properly
        var nextNumber = unchecked((uint)Interlocked.Increment(ref _currentNumber));
        return (int)(nextNumber % _collection.Count);
    }
}

Usage example:

public class SomeClient
{
    private readonly RoundRobinCollection<ServerConfig> _serverConfigs;

    public SomeClient(List<ServerConfig> serverConfigs)
    {
        _serverConfigs = new RoundRobinCollection<ServerConfig>(serverConfigs);
    }

    public void DoSomething(){
       var serverConfig = _serverConfigs.GetNext();
       // Do something with current serverConfig
    }
}
Mark Lagendijk
  • 6,247
  • 2
  • 36
  • 24