在多线程程序设计中,线程安全是一个非常重要的概念。随着PLINQ和Task Parallel Library的出现,发现从一开始就考虑线程安全可以带来明显的性能优势。使用如ConcurrentDictionary这样的类可以大大节省时间,但有时可能会遇到需要同步但本身不是线程安全的集合。
当处理可能耗时的重复计算时,可能更好的选择是并行运行迭代。而对于更直接的算术运算,可能最好保持在单个线程中执行。
幸运的是,有一些现有的类可以帮助实现线程安全。例如,经常使用LazyInitializer.EnsureInitialized(ref item, ()=>{})来初始化访问器属性。但这种方法有一些严重的限制:
经过深入研究,升级了使用锁和Monitor.TryEnter的最佳实践。一直都知道双重检查锁定(DCL)线程安全模式,用于检查条件,如果条件存在,则获取(或尝试)锁,然后再次检查条件再执行。但直到Lambda表达式出现,才找到了一种方法来合并实用函数来处理它。
如果有一个可以被多个线程访问的类,需要对所有公共属性以及可能的一些私有属性应用线程安全。厌倦了编写:
readonly object _propA_lock = new Object();
然后努力最大化文件读写性能。这比想象的要困难得多,并且不是一个绝对的解决方案,但它有潜力显著提高吞吐量。仍然需要捕获并重试I/O异常,其中文件被应用程序之外的访问。也在避免I/O异常时遇到了困难,如果过于积极地清理ReaderWriterLockSlim对象...
提供了一个ReadWriteHelper类,它使用ReaderWriterLockSlim对象作为基于键的锁定机制。这是优化每个锁对象重用的方法。通过允许每个锁对象持续一段时间,它允许重用,并且只有在有意义的时候才会处置它们。
ThreadSafety暴露了一组静态方法,它们的行为类似于lock关键字,但具有一些额外的功能。包括有条件的超时锁定。
以下是一些使用ThreadSafety类的示例代码:
object value;
if (!dictionary.TryGetValue(key, out value)) {
lock (dictionary) {
if (!dictionary.TryGetValue(key, out value)) {
dictionary.Add(key, value = newValue);
}
}
}
简化为:
object value;
ThreadSafety.LockConditional(dictionary,
()=> !dictionary.TryGetValue(key, out value),
()=> dictionary.Add(key, value = newValue));
或者更性能优化的读写版本:
object value;
ThreadSafety.SynchronizeReadWrite(dictionary,
key,
()=> !dictionary.TryGetValue(key, out value),
()=> dictionary.Add(key, value = newValue),
5000 /* lock-timeout */,
false /* throw on error */);
注意:上述优化版本(需要一个key)已经测试过,并且与Dictionary
将LockCleanupDelay开放给进行实验和调整。延迟清理似乎是避免文件访问冲突和担心过度锁定的解决方案。可以将其设置为零,以便在每次运行后执行清理,但在测试中,这容易引发IoException。