【问题标题】:What is a best practice for making a class's properties thread safe?使类的属性线程安全的最佳实践是什么?
【发布时间】:2013-10-23 14:09:36
【问题描述】:

[编辑:看起来原始问题涉及双精度而不是整数。所以我认为如果我们将整数更改为双精度,这个问题就成立了。]

我很少遇到从多个线程中使用的类中读取整数属性的问题,该类有时会返回零值。初始化后值不会更改。

这个question 解决了这个问题。共识是,即使我正在访问一个整数,我也需要同步属性。 (一些原始答案已被删除)。我没有在那里选择答案,因为我还没有解决我的问题。

所以我对此进行了一些研究,但我不确定要使用 .Net 4 的哪种锁定机制,或者这些锁定是否应该在类本身之外。

这是我想使用的:

  public class ConfigInfo
  {
    private readonly object TimerIntervalLocker = new object();
    private int _TimerInterval;
    public int TimerInterval
    {
      get
      {
        lock (TimerIntervalLocker) {
          return _TimerInterval;
        }
      }
    }

    private int _Factor1;
    public int Factor1
    {
      set
      {
        lock (TimerIntervalLocker) {
          _Factor1 = value;
          _TimerInterval = _Factor1 * _Factor2;
        }
      }
      get
      {
        lock (TimerIntervalLocker) {
          return _Factor1;
        }
      }
    }

    private int _Factor2;
    public int Factor2
    {
      set
      {
        lock (TimerIntervalLocker) {
          _Factor2 = value;
          _TimerInterval = _Factor1 * _Factor2;
        }
      }
      get
      {
        lock (TimerIntervalLocker) {
          return _Factor2;
        }
      }
    }
  }

但我听说这非常慢。

另一种选择是在用户端锁定ConfigData 的实例,但这似乎需要做很多工作。我见过的另一种选择是Monitor.EnterMonitor.Exit,但我认为 Lock 是相同的东西,语法更少。

那么制作类的属性线程的最佳实践是什么 安全吗?

【问题讨论】:

  • 如果您在初始化后不改变状态(初始化被定义为在其他线程使用该类之前),那么您的代码已经完全是线程安全的,没有任何锁。
  • 最好开发这样的设计来避免这种情况。我会先研究整个事情的设计。
  • @RichShealer 如果TimerInterval 永远为零,则只有两种可能性:(1)您在两个因素都设置为非零值之前访问它,或者(2)正在设置一个或两个因素为零。没有其他可能性(除了极不可能的因素,即因子设置为当相乘时会溢出导致为零的值)
  • @RichShealer 这对公共设置器来说不是一个好例子——听起来你需要在构造函数中设置只读字段。
  • “我听说这太慢了”——不要通过阅读别人的代码来进行性能分析。通过运行代码进行性能分析!一个非竞争锁大约需要 10-100 纳秒。争用锁是任意缓慢的。正确的解决方案几乎总是重构您的程序以避免争用,并采取 10-100 ns 的命中率。

标签: c# multithreading c#-4.0 properties thread-safety


【解决方案1】:

一个。使用锁可能会很慢,因为它使用操作系统资源,如果属性的复杂性较低,那么自旋锁(或 interlocked.compareexchange)会更快。

b.您必须确保线程不会进入锁,并通过从一个属性调用另一个属性被锁定。 - 如果这可能发生(当前不是代码中的问题),您需要使锁线程或任务敏感。

编辑:

如果对象应该在初始化期间设置并且从不更改,则使其不可变(就像 .NET 字符串一样)。删除所有公共设置器并提供一个带有参数的构造函数,用于定义初始状态,可能还有其他方法/操作符用于创建具有修改状态的新实例(例如 var newString = "Old string" + " was modified.";)。

【讨论】:

  • “使用锁很慢” - 这是非常非常难以衡量的,因为它取决于许多非确定性因素。
  • @BryanCrosby 是的,但我不想参加冗长的理论讲座。我会改写这个。
【解决方案2】:

如果值永远不会改变,那么只制作该实例的副本并将每个线程传递一个它自己的实例会更容易。完全不需要锁定。

【讨论】:

    【解决方案3】:

    我认为你应该重写你的 ConfigInfo 类,使其看起来像这样;那么你就不会出现溢出或线程问题:

    public sealed class ConfigInfo
    {
        public ConfigInfo(int factor1, int factor2)
        {
            if (factor1 <= 0)
                throw new ArgumentOutOfRangeException("factor1");
    
            if (factor2 <= 0)
                throw new ArgumentOutOfRangeException("factor2");
    
            _factor1 = factor1;
            _factor2 = factor2;
    
            checked
            {
                _timerInterval = _factor1*_factor2;
            }
        }
    
        public int TimerInterval
        {
            get
            {
                return _timerInterval;
            }
        }
    
        public int Factor1
        {
            get
            {
                return _factor1;
            }
        }
    
        public int Factor2
        {
            get
            {
                return _factor2;
            }
        }
    
        private readonly int _factor1;
        private readonly int _factor2;
        private readonly int _timerInterval;
    }
    

    请注意,我使用checked 来检测溢出问题。

    否则某些值会给出不正确的结果。

    例如,57344 * 524288 将在未经检查的整数算术中给出零(还有很多其他值对会给出零,甚至更多会给出负结果或“似乎”正确的正值)。

    【讨论】:

      【解决方案4】:

      如 cmets 中所述,最好将属性设置为只读。我想到了以下可能:

      public class ConfigInfo
      {
          private class IntervalHolder
          {
              public static readonly IntervalHolder Empty = new IntervalHolder();
      
              private readonly int _factor1;
              private readonly int _factor2;
              private readonly int _interval;
      
              private IntervalHolder()
              {
              }
      
              private IntervalHolder(int factor1, int factor2)
              {
                  _factor1 = factor1;
                  _factor2 = factor2;
                  _interval = _factor1*_factor2;
              }
      
              public IntervalHolder WithFactor1(int factor1)
              {
                  return new IntervalHolder(factor1, _factor2);
              }
      
              public IntervalHolder WithFactor2(int factor2)
              {
                  return new IntervalHolder(_factor1, factor2);
              }
      
              public int Factor1
              {
                  get { return _factor1; }
              }
      
              public int Factor2
              {
                  get { return _factor2; }
              }
      
              public int Interval
              {
                  get { return _interval; }
              }
      
              public override bool Equals(object obj)
              {
                  var otherHolder = obj as IntervalHolder;
                  return 
                      otherHolder != null &&
                      otherHolder._factor1 == _factor1 &&
                      otherHolder._factor2 == _factor2;
              }
          }
      
          private IntervalHolder _intervalHolder = IntervalHolder.Empty;
      
          public int TimerInterval
          {
              get { return _intervalHolder.Interval; }
          }
      
          private void UpdateHolder(Func<IntervalHolder, IntervalHolder> update)
          {
              IntervalHolder oldValue, newValue;
              do
              {
                  oldValue = _intervalHolder;
                  newValue = update(oldValue);
      
              } while (!oldValue.Equals(Interlocked.CompareExchange(ref _intervalHolder, newValue, oldValue)));
          }
      
          public int Factor1
          {
              set { UpdateHolder(holder => holder.WithFactor1(value)); }
              get { return _intervalHolder.Factor1; }
          }
      
          public int Factor2
          {
              set { UpdateHolder(holder => holder.WithFactor2(value)); }
              get { return _intervalHolder.Factor2; }
          }
      }
      

      这样,您的 TimerInterval 值始终与其因素同步。唯一的问题是当某个线程读取其中一个属性而另一个线程从 ConfigInfo 之外写入它们时。第一个可能会得到错误的值,如果不引入单个锁根,我看不到任何解决此问题的方法。问题是读取操作是否至关重要。

      【讨论】:

        猜你喜欢
        • 2011-03-17
        • 1970-01-01
        • 2010-09-12
        • 1970-01-01
        • 2019-12-31
        • 2010-10-30
        • 2010-09-08
        • 2010-10-17
        • 2017-09-01
        相关资源
        最近更新 更多