从线程安全对象读取C#和并发?

时间:2012-08-10 12:10:03

标签: c# .net multithreading thread-safety locking

处理线程安全对象 - 例如:Cache Object

Msdn说:

  

线程安全:此类型是线程安全的。

我确实在多线程环境中通过其中的键来实现设置的含义。

但是,在多线程环境中按键获取 呢?

如果200个线程想要读取值,199其他线程是否被阻塞/等待?

感谢您提供任何帮助。

5 个答案:

答案 0 :(得分:2)

提供线程安全性有不同的方法,并且未指定使用的方法,但它可以在不需要锁定读取的情况下发生。简短的回答,因为它的结构方式,所以不需要锁定。答案很长:

所谓的承诺&#34;所有方法都是线程安全的&#34;,如果同时调用这些方法,它们不会破坏对象或返回错误的结果。让我们考虑一下对象List<char>

假设我们有一个名为List<char>的空list,并且三个线程同时调用以下内容:

  1. char c = list[0]
  2. list.Add('a');
  3. list.Add('b');
  4. 有四种可能的正确结果:

    1. c包含'a'list包含{'a', 'b'}list.Count现在会返回2
    2. c包含'b'list包含{'b', 'a'}list.Count现在会返回2
    3. 在第一个帖子上引发ArgumentOutOfRangeExceptionlist包含{'a', 'b'}list.Count现在会返回2
    4. 在第一个帖子上引发ArgumentOutOfRangeExceptionlist包含{'b', 'a'}list.Count现在会返回2
    5. 所有这些都是正确的,因为列表是列表,我们必须在另一个之前插入一个项目。我们还必须以list[0]返回的值为最后一个。我们还必须将线程1的读取视为在插入之后或之前发生的事情,因此我们将第一个元素放入c或者我们抛出ArgumentOutOfRangeException(不是缺陷)使用线程安全性,查看空列表的第一项是否超出范围,无论线程问题如何。

      因为List<char>不是线程安全的,所以可能发生以下情况:

      1. 上述可能性之一(我们很幸运)。
      2. list包含{'a'}list.Count返回1. c包含'b'
      3. list包含{'b'}list.Count返回1. c包含'a'
      4. list包含{'a'}list.Count返回2. c包含'b'
      5. list包含{'b'}list.Count返回2. c包含'a'
      6. 插入调用中的一个或两个和/或读取调用将引发IndexOutOfRangeException。 (注意,不是第一个线程可以接受的ArgumentOutOfRangeException行为)。
      7. 断言失败。
      8. 上述任何一项(包括正确的行为),然后半小时后对list执行的另一项操作引发了一些异常,List<char>未记录为该方法的提升。
      9. 换句话说,我们完全搞砸了整个事情,以至于list处于程序员认为不可能进入的状态,并且所有的赌注都已关闭

        好的,我们如何构建线程安全的集合。

        一种简单的方法是锁定每个方法,这些方法要么改变状态,要么取决于状态(往往是所有方法)。对于静态方法,我们锁定静态字段,例如我们锁定实例字段的方法(我们可以锁定静态字段,但对单独对象的操作会相互阻塞)。我们确保如果一个公共方法调用另一个公共方法,它只锁定一次(可能通过让每个公共方法调用一个不会锁定的私有工作方法)。可能的一些分析表明我们可以将所有操作拆分为两个或多个彼此独立的组,并且每个组都有单独的锁,但不太可能。

        优点是简单,减号是锁定了很多,可能阻止了许多相互安全的操作。

        另一个是共享排他锁(正如.NET提供的错误名称ReaderWriterLockReaderWriterLockSlim - 错误命名,因为除了单一编写器多重读取器之外还有其他一些情况,包括实际上恰恰相反)。通常情况下,只读操作(从内部看 - 使用memoisation的只读操作实际上不是只读操作,因为它可能更新内部缓存)可以安全地同时运行,但是写操作必须是唯一的操作,并且共享独占锁可以让我们确保。

        另一种是通过条纹锁定。这就是ConcurrentDictionary当前实现的工作原理。创建了几个锁,并为每个写操作分配了一个锁。虽然其他写入可能同时发生,但那些会相互伤害的写入将尝试获得相同的锁定。某些操作可能需要获取所有锁定。

        另一种是通过无锁同步(有时称为&#34;低锁&#34;因为所使用的基元的特征在某些方面类似于锁而在其他方面不相似)。 https://stackoverflow.com/a/3871198/400547显示了一个简单的无锁队列,并且ConcurrentQueue再次使用了不同的无锁技术(在.NET中,上次我看起来Mono&s接近该示例)。

        虽然无锁方法可能会自动听起来像是最好的方法,但情况并非总是如此。在https://github.com/hackcraft/Ariadne/blob/master/Collections/ThreadSafeDictionary.cs我自己的无锁词典在某些情况下比ConcurrentDictionary条纹锁定更好,但在许多其他情况下却不如此。为什么?嗯,一开始ConcurrentDictionary几乎肯定有更多的时间用于调整它,而不是我能够给我的!但是,严肃地说,确保无锁方法是安全的,需要逻辑,其成本周期和内存与任何其他代码相同。有时候它超过了锁的成本,有时它并没有。 (编辑:目前,我现在在各种情况下击败了ConcurrentDictionary,但是ConcurrentDictioanry背后的人都很好,所以在下次更新的情况下,余额可能会更多地回归到他们的青睐)

        最后,有一个简单的事实,即某些结构对于某些操作本身就是线程安全的。 int[]例如是线程安全的,因为如果有几个线程正在读取和写入myIntArray[23],那么虽然有各种可能的结果,但它们都没有破坏结构和所有读取将看到初始状态或其中一个线程正在编写的值之一。您可能希望使用内存屏障来确保读取器线程在最后一个写入器完成后很长时间仍然看不到初始状态,因为CPU缓存过时。

        现在,int[]的真实情况对于object[]也是如此(但请注意,值类型的数组大于其运行的CPU将以原子方式处理的数组的类型不正确)。由于int[]object[]内部用于某些基于字典的类Hashtable并且在此特别感兴趣,Cache是示例 - 因此可以通过这种方式构建它们对于单个编写器线程和多个读取器线程,结构自然是线程安全的(不适用于多个编写器,因为编写器必须不时地调整内部存储的大小并且更难以使线程安全)。 Hashtable就是这种情况(对于某些类型Dictionary<TKey, TValue>TKeyTValue也属于TKey,但行为无法保证,对于TValue和{{1}})的其他类型,肯定不是这样。

        一旦你拥有一个对多个读者/单个编写者来说是自然线程安全的结构,那么为多个读者/多个编写者提供线程安全就是锁定写入的问题。

        最终结果是,您已获得线程安全的所有好处,但成本根本不会影响读者。

答案 1 :(得分:1)

线程安全并不意味着实现线程安全的任何特定策略(例如锁定)。它只是说如果多个线程与对象交互,那么它将继续产生其签约行为 - 合同是每个公共或受保护成员的记录行为。

如果没有进一步的文档,那么它是关于如何实现此线程安全性的实现细节,可能会有所变化。

当前可以对每次访问进行独占锁定。或者它可能使用无锁技术。它没有记录任何方式,但如果 在读取操作期间采取了独占锁,我会非常惊讶。

答案 2 :(得分:0)

据我了解,当一个人写入对象时,其他线程被阻止 在读取对象时阻塞其他线程没有任何意义 - 因为读取操作不会更改对象状态。

答案 3 :(得分:0)

线程安全不适用于阅读。缓存是线程安全的删除/插入记录。多个线程将能够同时读取值。如果在线程读取它时删除了值,那么在删除之后进入的值将只从缓存中获取空值(缓存未命中)。

答案 4 :(得分:0)

对于并发编写,.NET 4有一个ConcurrentQueue集合,可以很好地工作。我已经多次使用它并且从未遇到过任何问题。至于阅读我已经使用List集合进行并行处理并返回正确的值。请参阅下面我如何使用它的一个非常基本的例子。

List<string> collection = new List<string>(); // Empty list in this example
ConcurrentQueue<string> concurrent = new ConcurrentQueue<string>();

Parallel.ForEach(collection, new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount}, item =>
{
    concurrent.Enqueue(item);
});

虽然回答你的问题,但是当同时阅读时,集合不会锁定其他线程。但是,如果您正在写入队列(同时),则很可能某些项目可能被踢出而未插入。 'lock'可用于避免这种情况,但这会暂时阻止其他线程。