如何轻松使这个计数器属性线程安全?

时间:2012-01-25 23:20:15

标签: c# thread-safety

我在类中有属性定义,我只有计数器,这必须是线程安全的,这不是因为getset不在同一个锁中,怎么做?< / p>

    private int _DoneCounter;
    public int DoneCounter
    {
        get
        {
            return _DoneCounter;
        }
        set
        {
            lock (sync)
            {
                _DoneCounter = value;
            }
        }
    }

5 个答案:

答案 0 :(得分:23)

如果您希望以DoneCounter = DoneCounter + 1保证不受竞争条件限制的方式实施该属性,则无法在该属性的实现中完成。该操作不是原子操作,实际上是三个不同的步骤:

  1. 检索DoneCounter
  2. 的值
  3. 添加1
  4. 将结果存储在DoneCounter
  5. 您必须防止在任何这些步骤之间发生上下文切换的可能性。锁定getter或setter内部将无济于事,因为该锁的范围完全存在于其中一个步骤(1或3)中。如果要确保所有三个步骤一起发生而不被中断,那么您的同步必须涵盖所有三个步骤。这意味着它必须在包含所有这三个的上下文中发生。这可能最终会成为不属于包含DoneCounter属性的任何类的代码。

    使用您的物体的人有责任照顾线程安全。通常,没有具有读/写字段或属性的类可以以这种方式“线程安全”。但是,如果您可以更改类的接口以便不需要setter,则可以使其更加线程安全。例如,如果您知道DoneCounter只增加和减少,那么您可以像这样重新实现它:

    private int _doneCounter;
    public int DoneCounter { get { return _doneCounter; } }
    public int IncrementDoneCounter() { return Interlocked.Increment(ref _doneCounter); }
    public int DecrementDoneCounter() { return Interlocked.Decrement(ref _doneCounter); }
    

答案 1 :(得分:4)

使用Interlocked类提供原子操作,即本LinqPad示例中的固有线程安全:

void Main()
{
    var counters = new Counters();
    counters.DoneCounter += 34;
    var val = counters.DoneCounter;
    val.Dump(); // 34
}

public class Counters
{
    int doneCounter = 0;
    public int DoneCounter
    {
        get { return Interlocked.CompareExchange(ref doneCounter, 0, 0); }
        set { Interlocked.Exchange(ref doneCounter, value); }
    }
}

答案 2 :(得分:2)

你究竟想用柜台做什么?锁对整数属性的影响并不大,因为整数的读写都是原子的,有或没有锁定。锁可以获得的唯一好处是增加了内存屏障;在读取或写入共享变量之前和之后使用Threading.Thread.MemoryBarrier()可以达到相同的效果。

我怀疑你真正的问题是你正试图做一些像“DoneCounter + = 1”这样的事情,即使有锁定,它也会执行以下事件序列:

  Acquire lock
  Get _DoneCounter
  Release lock
  Add one to value that was read
  Acquire lock
  Set _DoneCounter to computed value
  Release lock

不是很有用,因为值可能会在get和set之间发生变化。所需要的是一种在没有任何干预操作的情况下执行获取,计算和设置的方法。有三种方法可以实现:

  1. 在整个操作过程中获取并保持锁定
  2. 使用Threading.Interlocked.Increment向_Counter添加值
  3. 使用Threading.Interlocked.CompareExchange循环更新_Counter

使用这些方法中的任何一种,都可以根据旧值计算_Counter的新值,以这种方式保证写入的值基于_Counter在写入时的值。 / p>

答案 3 :(得分:2)

如果您不仅期望某些线程偶尔会同时写入计数器,但是许多线程将继续这样做,那么您希望有几个计数器,至少一个缓存线除了彼此,并有不同的线程写入不同的计数器,当你需要计数时将它们相加。

这可以使大多数线程彼此保持不同,从而阻止它们将每个其他值从内核中刷出,并相互减慢速度。 (除非你能保证每个线程保持独立,否则你仍然需要互锁。)

对于绝大多数情况,你只需要确保偶尔的争用不会弄乱数值,在这种情况下,Sean U的答案在各方面都更好(像这样的条纹计数器对于无争议的使用来说速度较慢)。

答案 4 :(得分:0)

您可以将_DoneCounter变量声明为“volatile”,以使其成为线程安全的。见:

http://msdn.microsoft.com/en-us/library/x13ttww7%28v=vs.71%29.aspx