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.
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 theCompareExchange
isn't doing anything, because the value was changed by the other thread. You can see this by adding a total to each loop:(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 usingInterlocked
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:
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: uselock
. There's nothing wrong withlock
, and indeed it is very optimized internally. Lock-free is not automatically the same as "fastest" or indeed "simplest".I've managed to achieve almost the same performance as lockstrategy using the following code: