处理线程安全对象 - 例如: 缓存对象
Msdn 指出:
线程安全:这种类型是线程安全的。
我确实意识到它在多线程环境中按键设置的意义。
但是在多线程环境中按键获取呢?
如果200
线程想要读取一个值,199
其他线程是否被阻塞/等待?
感谢您提供任何帮助。
处理线程安全对象 - 例如: 缓存对象
Msdn 指出:
线程安全:这种类型是线程安全的。
我确实意识到它在多线程环境中按键设置的意义。
但是在多线程环境中按键获取呢?
如果200
线程想要读取一个值,199
其他线程是否被阻塞/等待?
感谢您提供任何帮助。
提供线程安全的方式有很多种,没有指定使用哪种方式,但它可以在不需要锁定读取的情况下发生。简短的回答,读取不需要锁,因为它的结构方式。长答案:
在说“所有方法都是线程安全的”时做出的承诺是,如果同时调用这些方法,它们不会破坏对象或返回不正确的结果。让我们考虑对象List<char>
。
假设我们有一个空调用,三个线程同时List<char>
调用list
以下内容:
char c = list[0]
.list.Add('a');
list.Add('b');
有四种可能的正确结果:
c
包含'a'
. list
包含{'a', 'b'}
. list.Count
现在会回来2
。c
包含'b'
. list
包含{'b', 'a'}
. list.Count
现在会回来2
。ArgumentOutOfRangeException
在第一个线程上引发。list
包含{'a', 'b'}
. list.Count
现在会回来2
。ArgumentOutOfRangeException
在第一个线程上引发。list
包含{'b', 'a'}
. list.Count
现在会回来2
。所有这些都是正确的,因为列表是一个列表,我们必须在另一个之前插入一个项目。我们还必须将最终成为第一个的值作为返回的值list[0]
。我们还必须将线程 1 的读取视为在此插入之后或之前发生,因此我们要么放入第一个元素,要么c
抛出ArgumentOutOfRangeException
(不是线程安全的缺陷,查看空的第一项list 超出范围,无论线程问题如何。
然而,由于List<char>
不是线程安全的,因此可能会发生以下情况:
list
包含{'a'}
. list.Count
返回 1.c
包含'b'
list
包含{'b'}
. list.Count
返回 1.c
包含'a'
list
包含{'a'}
. list.Count
返回 2.c
包含'b'
list
包含{'b'}
. list.Count
返回 2.c
包含'a'
IndexOutOfRangeException
. (注意,不是ArgumentOutOfRangeException
第一个线程引发的可接受行为)。list
半小时后执行的另一个操作会引发一些异常,该异常List<char>
未记录为针对该方法引发的异常。换句话说,我们完全把整个事情搞砸了,list
以至于程序员认为它不可能处于一种状态,并且所有的赌注都被取消了。
好的,那么我们如何构建线程安全的集合。
一种简单的方法是锁定每个更改状态或依赖于状态(往往是所有状态)的方法。对于静态方法,我们锁定一个静态字段,例如我们锁定一个实例字段的实例方法(我们可以锁定一个静态字段,但对单独对象的操作会相互阻塞)。我们确保如果一个公共方法调用另一个公共方法,它只锁定一次(也许通过让公共方法每个调用一个不锁定的私有工作方法)。有可能一些分析表明我们可以将所有操作分成两个或多个相互独立的组,并为每个组拥有单独的锁,但不太可能。
优点是简单,缺点是它锁定了很多,可能会阻止许多相互安全的操作。
另一个是共享排他锁(因为 .NET 通过错误命名ReaderWriterLock
和ReaderWriterLockSlim
- 错误命名提供,因为除了单写多读之外还有其他场景,包括一些实际上完全相反的场景)。只读操作(从内部看 - 使用记忆的只读操作并不是真正的只读操作,因为它可能会更新内部缓存)可以安全地同时运行,但写操作必须是唯一发生的操作,共享独占锁让我们可以确保这一点。
另一种是通过条带锁定。这就是当前实施的ConcurrentDictionary
工作方式。创建了几个锁,并为每个写操作分配一个锁。虽然其他写入可能同时发生,但那些会互相伤害的写入将试图获得相同的锁。某些操作可能需要获取所有锁。
另一种是通过无锁同步(有时称为“低锁”,因为所使用的原语的某些特性在某些方面类似于锁,但在其他方面则不然)。https://stackoverflow.com/a/3871198/400547显示了一个简单的无锁队列,并ConcurrentQueue
再次使用了不同的无锁技术(在.NET 中,我上次查看 Mono 时与该示例很接近)。
虽然无锁方法可能听起来像是最好的方法,但并非总是如此。我自己在https://github.com/hackcraft/Ariadne/blob/master/Collections/ThreadSafeDictionary.cs上的无锁字典在某些情况下比ConcurrentDictionary
's 条带锁定更好,但在许多其他情况下则不如。为什么?好吧,首先ConcurrentDictionary
几乎可以肯定的是,我花在调音上的时间比我所能提供的还多!不过说真的,确保无锁方法是安全的,需要与任何其他代码一样消耗周期和内存的逻辑。有时它超过了锁的成本,有时则不然。(编辑:目前,我现在ConcurrentDictionary
在各种各样的情况下都击败了,但是背后的人ConcurrentDictioanry
很好,所以在下一次更新时,平衡可能会在更多情况下恢复到他们的青睐)。
最后,有一个简单的事实,即某些结构对于某些操作本身是线程安全的。int[]
例如是线程安全的,就好像有几个线程正在读写,myIntArray[23]
虽然有多种可能的结果,但它们都不会破坏结构,所有读取都将看到初始状态,或者其中一个其中一个线程正在写入的值。尽管由于 CPU 缓存陈旧,您可能希望使用内存屏障来确保读取器线程在最后一个写入器完成后很长时间仍未看到初始状态。
现在,适用的int[]
情况也适用object[]
(但请注意,对于值类型大于其运行的 CPU 将以原子方式处理的数组的数组,情况并非如此)。由于int[]
和object[]
在一些基于字典的类内部使用 -Hashtable
并且在这里特别感兴趣,Cache
作为示例 - 可以以这样的方式构造它们,使得结构对于单个写入器线程和多个读取器线程(而不是多个写入器,因为作家时不时地必须调整内部存储的大小,这更难实现线程安全)。这是Hashtable
(IIRC)的情况,Dictionary<TKey, TValue>
对于某些类型也是如此,TKey
并且TValue
TKey
, 但不能保证这种行为,而且对于和TValue
)的某些其他类型肯定不是真的。
一旦你的结构对于多个读取器/单个写入器来说是自然线程安全的,那么让它对多个读取器/多个写入器来说是线程安全的就是锁定写入的问题。
最终结果是,您获得了线程安全的所有好处,但成本根本不会影响读者。
线程安全不适用于阅读。缓存对于删除/插入记录是线程安全的。多个线程将能够同时读取一个值。如果值在线程读取时被删除,则在删除后进入的值只会从缓存中获取空值(缓存未命中)。
线程安全并不意味着实现线程安全的任何特定策略(例如锁定)。它只是说,如果多个线程正在与对象交互,那么它将继续产生其约定的行为——其中约定是每个公共或受保护成员的记录行为。
如果没有进一步的文档,那么它是关于如何实现线程安全的实现细节,并且可能会发生变化。
目前,它可以对每次访问都采取排他锁。或者它可能使用无锁技术。无论哪种方式都没有记录,但如果在读取操作期间出现排他锁,我会感到非常惊讶。
据我了解,其他线程在写入对象时被阻塞。
在读取对象时阻塞其他线程没有任何意义——因为读取操作不会改变对象状态。
对于并发编写,.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' 可以用来避免这种情况,但这会暂时阻塞其他线程。