【问题标题】:Is This a Good Design for Creating Thread-Safe Classes in C#?这是在 C# 中创建线程安全类的好设计吗?
【发布时间】:2010-01-25 20:04:40
【问题描述】:

通常,当我想要一个线程安全的类时,我会执行以下操作:

public class ThreadSafeClass
{
    private readonly object theLock = new object();

    private double propertyA;
    public double PropertyA
    {
        get
        {
            lock (theLock)
            {
                return propertyA;
            }
        }
        set
        {
            lock (theLock)
            {
                propertyA = value;
            }
        }
    }

    private double propertyB;
    public double PropertyB
    {
        get
        {
            lock (theLock)
            {
                return propertyB;
            }
        }
        set
        {
            lock (theLock)
            {
                propertyB = value;
            }
        }
    }

    public void SomeMethod()
    {
        lock (theLock)
        {
            PropertyA = 2.0 * PropertyB;
        }
    }
}

它有效,但它非常冗长。有时我什至会为每个方法和属性创建一个锁对象,从而产生更多的冗长和复杂性。

我知道也可以使用 Synchronization 属性锁定类,但我不确定它的扩展性如何——因为我经常期望有数十万甚至数百万个线程安全对象实例.这种方法将为类的每个实例创建一个同步上下文,并要求该类从 ContextBoundObject 派生,因此不能从其他任何东西派生——因为 C# 不允许多重继承——这是一个显示停止器在很多情况下。

编辑:正如一些响应者所强调的,没有“灵丹妙药”线程安全类设计。我只是想了解我使用的模式是否是好的解决方案之一。当然,任何特定情况下的最佳解决方案都取决于问题。以下几个答案包含应考虑的替代设计。

编辑:此外,线程安全的定义不止一种。例如,在我上面的实现中,以下代码不是线程安全的:

var myObject = new ThreadSafeClass();
myObject.PropertyA++; // NOT thread-safe

那么,上面的类定义代表了一个好的方法吗?如果不是,对于具有类似行为的设计,对于一组类似的用途来说是线程安全的,您会推荐什么?

【问题讨论】:

  • 请先搜索:stackoverflow.com/…
  • 嗯。在您的代码中,您使用相同的锁定对象来读取 PropertyA 和 PropertyB。这意味着读取 PropertyA 的一个线程将锁定另一个尝试读取 PropertyB 的线程,直到第一个线程完成。那是你要的吗?如果这是我的课,我会为每个 getter 创建一个单独的读取锁,然后设置器也必须尊重该锁。请参阅下面的答案。
  • 我只是对你投了反对票,因为没有单一的方法可以生成线程安全的类。对这个问题的任何回答都有可能给未来的读者留下错误的印象。
  • 约翰,我同意你的观点,并根据你的评论编辑了我上面的问题。
  • 我的意思不是有多个解决方案——我的意思是线程安全的定义不止一个。您的问题表明只有一个。

标签: c# .net multithreading thread-safety


【解决方案1】:

对于多线程问题没有“一刀切”的解决方案。对创建不可变类进行一些研究并了解不同的同步原语。

这是 semi-immutablethe-programmers-immutable 类的示例。

public class ThreadSafeClass
{
    public double A { get; private set; }
    public double B { get; private set; }
    public double C { get; private set; }

    public ThreadSafeClass(double a, double b, double c)
    {
        A = a;
        B = b;
        C = c;
    }

    public ThreadSafeClass RecalculateA()
    {
        return new ThreadSafeClass(2.0 * B, B, C);
    }
}

此示例将您的同步代码移动到另一个类并序列化对实例的访问。实际上,您真的不希望在任何给定时间对一个对象进行多个线程操作。

public class ThreadSafeClass
{
    public double PropertyA { get; set; }
    public double PropertyB { get; set; }
    public double PropertyC { get; set; }

    private ThreadSafeClass()
    {

    }

    public void ModifyClass()
    {
        // do stuff
    }

    public class Synchronizer
    {
        private ThreadSafeClass instance = new ThreadSafeClass();
        private readonly object locker = new object();

        public void Execute(Action<ThreadSafeClass> action)
        {
            lock (locker)
            {
                action(instance);
            }
        }

        public T Execute<T>(Func<ThreadSafeClass, T> func)
        {
            lock (locker)
            {
                return func(instance);
            }
        }
    }
}

下面是一个简单的示例,说明您将如何使用它。它可能看起来有点笨拙,但它允许您一次在实例上执行许多操作。

var syn = new ThreadSafeClass.Synchronizer();

syn.Execute(inst => { 
    inst.PropertyA = 2.0;
    inst.PropertyB = 2.0;
    inst.PropertyC = 2.0;
});

var a = syn.Execute<double>(inst => {
    return inst.PropertyA + inst.PropertyB;
});

【讨论】:

  • 我理解您的方法——而且我认为在许多情况下它会是最好的方法。但是,它不允许使用属性语法更改 A、B 和 C,如果更新非常频繁,则会产生性能问题,因为每次更新都需要创建一个新对象并让 GC 处理旧对象。
  • 不适合这样的课程。但对于任何类型的业务对象,您都是正确的。
  • 这只是一个不可变类的例子。虽然说它是“线程安全的”在技术上是正确的(从某种意义上说,与多个同时线程的交互不会导致损坏或错误行为),但它几乎不是“这就是所有‘线程安全’类的构建方式”的模型.
  • 线程安全类没有“一刀切”的模型。这和我的更新是众多可能性中的两种。
  • +1:不可变类确实是线程安全类的最佳通用解决方案,尤其是如果您只想编写类并且甚至不想考虑是否已实现线程安全正确。
【解决方案2】:

我知道这听起来像是一个聪明的答案,但是......开发线程安全类的最佳方法是真正了解多线程、它的含义、它的复杂性以及它的含义。没有灵丹妙药。

说真的...不要尝试多线程(我的意思是在生产场景中),直到您知道自己要进入什么...这可能是一个巨大的错误。

编辑:你当然应该知道操作系统和你选择的语言的同步原语(我猜在这种情况下是 Windows 下的 C#)。

很抱歉,我不只是提供代码来使类线程安全。那是因为它不存在。一个完全线程安全的类可能会比仅仅避免线程要慢,并且可能会成为您正在做的任何事情的瓶颈......有效地撤消您通过使用线程实现的任何事情。

【讨论】:

  • 我知道没有灵丹妙药。我有很多使用我上面描述的模式的生产代码——我同意你的观点,多线程不应该掉以轻心。但是,我的应用程序是健壮的,并且(当前)没有遇到任何死锁或竞争条件——尽管它在过去有过。我并不是说这种模式或它的变体是所有多线程问题的答案。我只是问是否有更优雅的方法来获得上述代码的行为。对我来说,这似乎并不理想,我正在努力利用社区的智慧。
  • 我不得不说这是非常糟糕的建议。这就好比说,每次拿起锯子之前,都要先成为工匠大师。你必须从某个地方开始,这不是一个糟糕的地方。
  • 我必须同意乔纳森的观点。
  • 我认为 Edit: 上面的部分是非常好的一般建议,因为多线程可能看似复杂。以@JonathanAllen 的类比,我想说这更像是“在使用锯子制作实际家具之前了解回扣和撕裂”。
  • 我明确声明不要在没有适当知识的情况下尝试在生产场景中执行此操作。多线程是开发世界中最复杂的主题之一(与加密一起),这个类比更适合外科手术。在对真正的患者进行手术之前,您最好对需要了解的所有内容有一个真正的了解,否则您最终会杀死您的患者(您的项目)。当然,继续练习尸体来学习(做有趣的项目来学习),但如果你没有适当的知识,就不要在生产代码上练习。
【解决方案3】:

请记住,术语“线程安全”不是特定的;通过使用Monitor 锁,您在这里所做的更准确地称为“同步”。

也就是说,同步代码的冗长几乎是不可避免的。您可以通过以下方式减少示例中的一些空白:

lock (theLock)
{
    propertyB = value;
}

进入这个:

lock (theLock) propertyB = value;

至于这是否是适合您的正确方法,我们确实需要更多信息。同步只是“线程安全”的一种方法;不可变对象、信号量等都是适合不同用例的不同机制。对于您提供的简单示例(看起来您正在尝试确保 get 或 set 操作的原子性),那么看起来您已经做了正确的事情,但如果您的代码旨在更多插图而不是示例,那么事情可能没有那么简单。

【讨论】:

    【解决方案4】:

    由于似乎没有其他人这样做,这里是对您的具体设计的一些分析。

    • 想要读取任何单个属性?线程安全
    • 想要更新到任何单个属性?线程安全
    • 想要读取单个属性,然后根据其原始值对其进行更新?不是线程安全的

    线程 2 可以在线程 1 的读取和更新之间更新值。

    • 想要同时更新两个相关属性?不是线程安全的

    您最终可能会得到属性 A 具有线程 1 的值,而属性 B 具有线程 2 的值。

    1. 线程 1 更新 A
    2. 线程 2 更新 A
    3. 线程 1 更新 B
    4. 线程 2 更新 B

      • 想要同时读取两个相关属性?不是线程安全的

    同样,您可能会在第一次和第二次阅读之间被打断。

    我可以继续,但你明白了。线程安全完全取决于您计划如何访问对象以及您需要做出什么承诺。

    【讨论】:

      【解决方案5】:

      您可能会发现 Interlocked 课程很有帮助。它包含几个原子操作。

      【讨论】:

        【解决方案6】:

        您可以做的一件事可以帮助您避免额外的代码,那就是使用 PostSharp 之类的东西自动将这些 lock 语句注入您的代码中,即使您有数百个这样的语句。您只需要一个附加到类的属性,以及将添加额外锁定变量的属性实现。

        【讨论】:

          【解决方案7】:

          根据我上面的评论 - 如果您希望允许同时阅读但只允许一位作者,它会变得有点毛茸茸。请注意,如果您有 .NET 3.5,请使用 ReaderWriterLockSlim 而不是 ReaderWriterLock 来处理这种类型的模式。

          public class ThreadSafeClass
          {
              private readonly ReaderWriterLock theLock = new ReaderWriterLock();
          
              private double propertyA;
              public double PropertyA
              {
                  get
                  {
                      theLock.AcquireReaderLock(Timeout.Infinite);
                      try
                      {
                          return propertyA;
                      }
                      finally
                      {
                          theLock.ReleaseReaderLock();
                      }
                  }
                  set
                  {
                      theLock.AcquireWriterLock(Timeout.Infinite);
                      try
                      {
                          propertyA = value;
                      }
                      finally
                      {
                          theLock.ReleaseWriterLock();
                      }
                  }
              }
          
              private double propertyB;
              public double PropertyB
              {
                  get
                  {
                      theLock.AcquireReaderLock(Timeout.Infinite);
                      try
                      {
                          return propertyB;
                      }
                      finally
                      {
                          theLock.ReleaseReaderLock();
                      }
                  }
                  set
                  {
                      theLock.AcquireWriterLock(Timeout.Infinite);
                      try
                      {
                          propertyB = value;
                      }
                      finally
                      {
                          theLock.ReleaseWriterLock();
                      }
                  }
              }
          
              public void SomeMethod()
              {
                  theLock.AcquireWriterLock(Timeout.Infinite);
                  try
                  {
                      theLock.AcquireReaderLock(Timeout.Infinite);
                      try
                      {
                          PropertyA = 2.0 * PropertyB;
                      }
                      finally
                      {
                          theLock.ReleaseReaderLock();
                      }
                  }
                  finally
                  {
                      theLock.ReleaseWriterLock();
                  }
              }
          }
          

          【讨论】:

          • 如果你有 10 个属性而不是 2 个呢?!
          • 知道你的工具,框架有ReaderWriterLockSlim来执行这种锁:msdn.microsoft.com/en-us/library/…
          • 这正是我问您是否希望将属性完全同步在一起的原因。这一切都基于您的需求。你用灵活性换取复杂性。 Chris 对 ReaderWriterLockSlim(或 ReaderWriterLock,如果你想要 HEAVYWEIGHT 版本)的建议将有助于序列化作者。
          猜你喜欢
          • 1970-01-01
          • 2011-08-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-10-12
          相关资源
          最近更新 更多