问题
假设现在有一个键– 值对集合需要保持同步,比如内存缓存,不过有多个线程正在对其执行读写操作。
解决方案
.NET 框架中的 ConcurrentDictionary<TKey, TValue> 类型就是数据结构中的宝藏。它是线程安全的,混用细粒度锁和无锁技术,确保能在大多数场景中快速访问。另外,它的 API 需要花些功夫来熟悉。它必须处理来自多个线程的并发访问,这一点与标准的 Dictionary<TKey, TValue> 类型非常不同。但是,一旦学会了本节中的基础知识,就会发现 ConcurrentDictionary<TKey, TValue> 是非常实用的集合类型。
首先来看如何对集合写入值。可以通过 AddOrUpdate 实现给键赋值:var dictionary = new ConcurrentDictionary<int, string>();
string newValue = dictionary.AddOrUpdate(0,
key => "Zero",
(key, oldValue) => "Zero");AddOrUpdate 有一些复杂,它要根据并发字典当前的内容处理若干件事情。第 1 个参数是键,第 2 个参数是委托,通过委托将键(本例中为 0)转换为待添加至字典的值(本例中为“Zero”)。只有当字典中不存在该键时,才会调用该委托。第 3 个参数是另一个委托,它把键(0)和旧值转换为已更新的、待存入字典的值(“Zero”)。同样,只有当字典中不存在该键时,才会调用该委托。AddOrUpdate 会为该键返回新值,这个新值与任意委托返回的值一样。
接下来才是真正复杂的部分:为了能让并发字典稳妥地工作,AddOrUpdate 可能需要多次调用任意委托,或同时调用两个委托。这非常罕见,却是有可能发生的。因此,委托应该简单且迅捷,并且不会产生副作用。这意味着委托应该只创建值,而不改变应用程序中的任意其他变量。所有传入 ConcurrentDictionary<TKey, TValue> 的方法的委托,都同样遵循该原则。
还有若干种方法可以向字典中添加值,使用索引语句就是一种快捷方法:// 使用与前面相同的“字典”
// 添加(或更新)0键,赋值为"Zero"
dictionary[0] = "Zero";
索引语句的功能没那么强大,不能通过它基于现有值来更新一个值。然而,若有需要存入字典的值,这种语句就更为简单易用。
下面来看一下如何读取值。通过 TryGetValue 便很容易实现:// 使用与前面相同的“字典”
bool keyExists = dictionary.TryGetValue(0, out string currentValue);
如果在字典中找到 out 键,TryGetValue 就会返回 true,并且会给它赋值。相反,如果没有找到 out 键,TryGetValue 就会返回 false。也可以使用索引语句来读取值,但那种做法并不实用,这是因为它会在找不到键的情况下抛出异常。特别注意,并发字典有多个线程在读取、更新、添加和移除值,而且在许多情况下,在尝试读取某个键之前,根本无法知晓这个键是否存在。
移除值与读取值一样容易操作:// 使用与前面相同的“字典”
bool keyExisted = dictionary.TryRemove(0, out string removedValue);TryRemove 与 TryGetValue 几乎一致,唯一不同之处就是如果在字典中找到键,那么它会将键 –值对移除。
讨论
虽然 ConcurrentDictionary<TKey, TValue> 是线程安全的,但这并不意味着它是原子操作。如果两个线程并发调用 AddOrUpdate,那么两者可能都会检测到键的缺失,同时并发执行各自的委托来创建新值。ConcurrentDictionary<TKey, TValue> 很实用,这主要是因为有强大的 AddOrUpdate 方法。然而,它并非适用于所有情况。当有多个线程读写共享集合时,最好使用 ConcurrentDictionary<TKey, TValue>。但是,如果更新并非常态(相对较少),那么或许 ImmutableDictionary<TKey, TValue> 才是更好的选择。ConcurrentDictionary<TKey, TValue> 最适用于共享数据的情况,在这种情况下,多个线程共享相同的集合。如果某些线程只添加元素,而其他线程只移除元素,那么最好使用生产者集合(或消费者集合)。ConcurrentDictionary<TKey, TValue> 并非唯一的线程安全集合,BCL 也提供了 ConcurrentStack<T>、ConcurrentQueue<T> 以及 ConcurrentBag<T>。