Using Interlocked.CompareExchange to increment a c

2019-05-06 15:58发布

问题:

I need to increment a counter until it reaches a particular number. I can use two parallel task to increment the number. Instead of using a lock to check if the number has not reach the maximum allowed value and then incrementing, I thought using Interlocked.CompareExchange in the following manner:

public class CompareExchangeStrategy
{
  private int _counter = 0;
   private int _max;

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

public void Increment()
{
    Task task1 = new Task(new Action(DoWork));
    Task task2 = new Task(new Action(DoWork));
    task1.Start();
    task2.Start();
    Task[] tasks = new Task[2] { task1, task2 };
    Task.WaitAll(tasks);

}

private void DoWork()
{
    while (true)
    {
        int initial = _counter;
        if (initial >= _max)
        {
            break;
        }
        int computed = initial + 1;
        Interlocked.CompareExchange(ref _counter, computed, initial);
    }
}

 }

This code is taking more to execute (for _max= 1,000,000) than the lock approach:

public class LockStrategy
{
    private int _counter = 0;
    private int _max;

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

    public void Increment()
    {
        Task task1 = new Task(new Action(DoWork));
        Task task2 = new Task(new Action(DoWork));
        task1.Start();
        task2.Start();
        Task[] tasks = new Task[2] { task1, task2 };
        Task.WaitAll(tasks);

    }

    private void DoWork()
    {
        while (true)
            {
                lock (_lockObject)
                {
                    if (_counter < _max)
                    {
                        _counter++;
                    }
                    else
                    {
                        break;
                    }
                }
            }
    }

   }

There might be a problem with the way I am using Interlocked.CompareExchange but I have not been able to figure out. Is there a better way to perform the above logic without using lock (aka Interlocked methods)?


Update
I was able to come with a version which performs as good as the lock version (for iterations = 1,000,000 and better for > 1,000,000 iterations).

    SpinWait spinwait = new SpinWait();
    int lock =0;
                while(true)
                {

                    if (Interlocked.CompareExchange(ref lock, 1, 0) != 1)
                    {

                        if (_counter < _max)
                        {
                            _counter++;
                            Interlocked.Exchange(ref lock, 0);
                        }
                        else
                        {
                            Interlocked.Exchange(ref lock, 0);
                            break;
                        }

                    }
                    else
                    {
                        spinwait.SpinOnce();
                    }
                }


The difference is made by the spin. If the task is unable to increment the variable on first go it spins providing an opportunity for task 2 to progress further instead of performing a busy spin wait.

I suspect lock pretty much does the same, it could employ a strategy to spin and allow the thread currently owning the lock to execute.

回答1:

The problem here is that you are actually doing a lot more work in the Interlocked version - by which I mean more iterations. This is because a lot of the time the CompareExchange isn't doing anything, because the value was changed by the other thread. You can see this by adding a total to each loop:

    int total = 0;
    while (true)
    {
        int initial = Thread.VolatileRead(ref _counter);
        if (initial >= _max)
        {
            break;
        }
        int computed = initial + 1;
        Interlocked.CompareExchange(ref _counter, computed, initial);
        total++;
    }
    Console.WriteLine(total);

(note I also added a VolatileRead to ensure _counter isn't held in a register)

I get much more than iterations (via total) that you might expect here. The point is that when using Interlocked in this way, you need to add a strategy for what happens if the value changed, i.e. a retry strategy.

For example, a crude retry strategy might be:

    while (true)
    {
        int initial = Thread.VolatileRead(ref _counter);
        if (initial >= _max)
        {
            break;
        }
        int computed = initial + 1;
        if (Interlocked.CompareExchange(ref _counter, computed, initial)
                          != initial) continue;
        total++;
    }

which is to say: keep retrying until you make it work - any "doing" code would only happen after that check (where the total++ line is currently). This, however, makes the code more expensive.

If lock is cheaper: use lock. There's nothing wrong with lock, and indeed it is very optimized internally. Lock-free is not automatically the same as "fastest" or indeed "simplest".



回答2:

I've managed to achieve almost the same performance as lockstrategy using the following code:

public class CompareExchangeStrategy {
        volatile private int _counter = 0;
        private int _max;

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

        public void Increment() {
            Task task1 = new Task(new Action(DoWork));
            Task task2 = new Task(new Action(DoWork));
            task1.Start();
            task2.Start();
            Task[] tasks = new Task[2] { task1, task2 };
            Task.WaitAll(tasks);

        }

        private void DoWork() {
            while(true) {
                if(Interlocked.Add(ref _counter, 1) >= _max)
                    break;
            }
        }
    }