使用Interlocked.CompareExchange将计数器递增到一个值

时间:2013-03-19 09:17:34

标签: c# locking task-parallel-library task atomic

我需要增加一个计数器,直到达到一个特定的数字。我可以使用两个并行任务来增加数字。我没有使用锁来检查数字是否未达到允许的最大值然后递增,而是考虑使用Interlocked。以下列方式进行CompareExchange:

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);
    }
}

 }

此代码比锁定方法执行更多(对于_max = 1,000,000):

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;
                    }
                }
            }
    }

   }

我使用Interlocked.CompareExchange的方式可能有问题,但我一直无法理解。有没有更好的方法来执行上述逻辑而不使用锁(又名Interlocked方法)?

<小时/> 的更新
我能够提供一个性能与锁定版本一样好的版本(迭代次数= 1,000,000,更好的是> 1,000,000次迭代)。

    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();
                    }
                }


不同之处在于旋转。如果任务首先无法递增变量,那么它会旋转,从而为任务2提供进一步推进的机会,而不是执行繁忙的旋转等待。

我怀疑锁也是如此,它可以采用一种策略来旋转并允许当前拥有锁的线程执行。

2 个答案:

答案 0 :(得分:4)

这里的问题是你实际上在Interlocked版本中做了很多工作 - 我的意思是更多的迭代。这是因为CompareExchange 没有做任何事情的 很多 ,因为该值已被其他线程更改。你可以通过在每个循环中添加一个总数来看到这一点:

    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);

(注意我还添加了VolatileRead以确保_counter不在注册表中

我得到的很多比你在这里期望的迭代(通过total)更多。关键是,当以这种方式使用Interlocked时,您需要为值更改时发生的情况添加策略,即重试策略。

例如,原始重试策略可能是:

    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++;
    }

也就是说:继续重试,直到你完成它为止 - 任何“做”代码只会在检查后发生(当前total++行)。但是,这会使代码更加昂贵。

如果lock更便宜:请使用locklock没有任何问题,实际上它在内部进行了非常优化。无锁不会自动与“最快”或“最简单”相同。

答案 1 :(得分:-1)

我使用以下代码设法获得与lockstrategy几乎相同的性能:

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;
            }
        }
    }