2010-01-25 58 views
12

通常,当我想一类是线程安全的,我这样做如下:这是一个很好的设计在C#中创建线程安全类?

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 

因此,没有上面的类定义代表了一个好办法?如果不是,那么对于具有类似行为的设计,您会推荐哪种设计,这些设计对于类似的用途是线程安全的?

+4

请先搜索:http://stackoverflow.com/search?q=What+is+the+Best+Practice+for+Creating+Thread-Safe+Classes+in+C%23 – 2010-01-25 20:06:33

+0

嗯。在你的代码中,你使用相同的锁对象来读取PropertyA和PropertyB。这意味着一个读取PropertyA的线程会锁定另一个尝试读取PropertyB的内容,直到第一个线程完成。那是你要的吗?如果这是我的课程,我会为每个吸气器创建一个单独的读取锁定器,然后设置者也必须尊重该锁定。请参阅下面的答案。 – 2010-01-25 20:44:48

+0

我只是低估了你,因为没有单一的方法来产生一个线程安全的类。对这个问题的任何回答都有可能给未来的读者带来错误的印象。 – 2010-01-26 01:06:09

回答

8

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

这是一个半不可改变的程序员-不可改变类的一个例子。

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

我明白你的方法 - 我发现在很多情况下它会是最好的。但是,它不允许使用属性语法更改A,B和C,并且如果更新非常频繁,则会产生性能问题,因为每次更新都需要创建一个新对象并让GC处理旧对象。 – 2010-01-25 20:33:35

+0

不适合这样的班级。但对于任何类型的商业对象,您都是正确的。 – ChaosPandion 2010-01-25 20:36:01

+1

这只是一个不可变类的例子。虽然说它是“线程安全”的技术上是正确的(在多个同时线程的交互不会导致腐败或不良行为的意义上),但它并不是一种模型,“这就是所有'线程安全'类应该如何进行的” 。 – 2010-01-25 20:51:02

3

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

也就是说,围绕同步代码的冗长是非常不可避免的。

lock (theLock) 
{ 
    propertyB = value; 
} 

到这一点::您可以通过打开这样的事情削减一些在你的榜样空白的

lock (theLock) propertyB = value; 

至于这是否是适合你的正确方法,我们真的需要更多的信息。同步只是“线程安全”的一种方法;不可变对象,信号量等都是适合不同用例的不同机制。对于你提供的简单例子(看起来你试图确保获取或设置操作的原子性),那么它看起来像你做了正确的事情,但如果你的代码旨在更多的是举例而言,事情可能并非如此简单。

0

根据我上面的评论 - 如果你想允许同时读者但只有一个作家允许,它会变得有点多毛。请注意,如果您使用.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(); 
     } 
    } 
} 
+0

如果你有10个属性而不是两个? – 2010-01-25 20:52:00

+4

知道你的工具,框架有ReaderWriterLockSlim来执行这种锁定:http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx – 2010-01-25 20:59:42

+0

这正是我问你是否想要具有完全同步或不同步的属性。这完全取决于你的需求。您交易复杂性的灵活性。 Chris对ReaderWriterLockSlim(或者ReaderWriterLock,如果你想要HEAVYWEIGHT版本的)的建议将会使编写者序列化。 – 2010-01-25 21:49:07

1

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

1

有一两件事你可以做,可以帮助您避免额外的代码是使用类似PostSharp自动注入这些lock语句到你的代码,即使你有数百人。所有你需要的是一个属性附加到类,和属性的实现,将添加额外的锁定变量。

4

我知道这听起来像一个聪明的一个**答案,但...开发线程安全类是真正了解多线程的,关于它的意义,它的复杂性它有什么暗示的最佳途径。没有银弹。

说真的......不要尝试多线程(在生产场景中,我的意思是),直到你知道自己陷入了什么......它可能是一个巨大的错误。你应该知道操作系统和你选择的语言的同步原语(在这种情况下,我猜这是Windows下的C#)。

对不起,我不会放弃的只是代码,只是做一个类线程安全的。那是因为它不存在。一个完全线程安全的类可能会比仅仅避免线程慢,并且可能会成为任何你正在做的事情的瓶颈......有效地使用线程来取消你所做的任何事情。

+0

我知道没有银弹。我有很多使用上述模式的生产代码 - 我同意你不应轻视多线程。但是,我的应用程序非常强大,并且不会(目前)受到任何死锁或竞争条件的影响 - 尽管它已经在过去。我并不是说这种模式或变体就是解决所有多线程问题的答案。我只是问是否有更优雅的方式来获得上述代码所具有的行为。它对我来说似乎并不理想,我正在试图利用社区的智慧。 – 2010-01-26 03:32:37

+5

我不得不说这是非常糟糕的建议。这就好像是说每个人拿起锯子之前都应该成为一名高级工匠。你必须从某个地方开始,这不是一个糟糕的地方。 – 2010-01-26 03:36:15

+0

我必须同意乔纳森对此的看法。 – ChaosPandion 2010-01-26 04:02:54

3

由于没有其他人似乎在做它,这里是你的特殊设计的一些分析。

  • 想阅读单一的财产? Threadsafe
  • 想要更新到任何单个属性? Threadsafe
  • 想要读取一个属性,然后根据其原始值更新它?不是线程安全的

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

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

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

  1. 线程1更新
  2. 线程2更新
  3. 线程1个更新乙
  4. 线程2更新乙

    • 想在同一时间阅读两个相关的属性?不是线程安全的

同样,你可以在第一和第二读取之间中断。

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