问题
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;
}
}
}
来源:https://stackoverflow.com/questions/15495459/using-interlocked-compareexchange-to-increment-a-counter-until-a-value