65.9K
CodeProject 正在变化。 阅读更多。
Home

使线程安全同步更简单

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.17/5 (6投票s)

2011年11月22日

CPOL

4分钟阅读

viewsIcon

88495

downloadIcon

398

局部范围的委托和 Lambda 表达式允许一些非常智能且有用的实用函数。

介绍 

通常,我们在开始编写项目时不会考虑使其具有线程安全性。在很多情况下,这并不是真正必要的。但随着 PLINQ 和任务并行库 (Task Parallel Library) 的出现,我们发现从一开始就考虑“线程安全”具有明显的性能优势。使用 `ConcurrentDictionary` 等类可以节省大量时间,但我们也可能遇到需要同步但本身不具备线程安全性的集合。

注意事项

当处理重复的、可能耗时的计算时,最好并行运行这些迭代。对于更直接的算术运算,最好保持在单个线程中。

LazyInitializer

令人高兴的是,有一些现有的类可以帮助实现线程安全。在可能的情况下,我使用 `LazyInitializer.EnsureInitialized(ref item, ()=>{})` 来初始化我的访问器属性。但这有一些严重的限制:

  1. 它不允许返回 null。
  2. 它本身不允许同步其他属性。

线程安全模式

经过深入研究,我升级了我使用锁和 `Monitor.TryEnter` 的最佳实践。我一直都知道双重检查锁定 (DCL) 线程安全模式,即检查一个条件,如果条件存在,则获取(或尝试获取)锁,然后在执行前再次检查该条件。但在 Lambda 表达式出现之前,我一直没有找到一种方法来封装处理它的实用函数。

锁定类的所有属性

如果一个类可以被多个线程访问,你需要对所有公共属性,甚至可能是某些私有属性应用线程安全。我写得非常厌倦了

readonly object _propA_lock = new Object(); 

优化线程安全的磁盘 I/O (ReaderWriterLockSlim)

然后我努力最大化文件读/写性能。这比我想象的要困难得多,并且不是一个完美的解决方案,但它有潜力显著提高吞吐量。当文件被应用程序外部的其他进程访问时,你仍然需要捕获和重试 I/O 异常。如果我过于激进地清理 `ReaderWriterLockSlim` 对象,我也会遇到避免 I/O 异常的困难……

我提供了一个 `ReadWriteHelper` 类,它使用 `ReaderWriterLockSlim` 对象作为基于键的锁定机制。这是我优化每个锁对象重用的解决方案。通过让每个锁对象持续一段时间,可以实现重用,并且只在有意义时才将其释放。

`ReadWriteHelper` 继承自 `DeferredCleanupBase`,它使用计时器延迟清理直到需要时才执行。

最后,我提供了一些 `ReaderWriterLockSlim` 的扩展,以帮助防止读/写代码出错。

使用代码 

`ThreadSafety` 提供了一组静态方法,它们类似于 `lock` 关键字,但具有一些额外功能,包括带条件的超时锁定。它还允许比 `LazyInitializer` 提供的简单 null 值更复杂的条件。(请参阅“重要说明”了解正确的条件实现。)

object value;
if(!dictionary.TryGetValue(key,out value)) {
  lock(dictionary) {
    if(!dictionary.TryGetValue(key,out value)) {
      dictionary.Add( value = newValue );
    }
  }
}

简化为:

object value;
ThreadSafety.LockConditional( dictionary,
  ()=> !dictionary.TryGetValue(key, out value),
  ()=> dictionary.Add( value = newValue ) );

或者更性能优化的读/写版本:

object value;
ThreadSafety.SynchronizeReadWrite( dictionary, key,
  ()=> !dictionary.TryGetValue(key, out value),
  ()=> dictionary.Add( value = newValue ),
  5000 /*lock-timeout*/,
  false /*throw on error*/);    

注意:上面经过优化的版本(需要一个 **键**)经过测试,在应用于 `Dictionary<TKey,TValue>` 时,其性能与 `ConcurrentDictionary<TKey,TValue>` 内置的 `GetOrAdd` 方法相当。

...

`ThreadSafety.Helper` 也可以作为实例类进行初始化,它会自动安全地创建/利用锁。以下是我如何将其作为实例使用的:

// Default keys are strings but you can use whatever key type you like (int for example).
readonly ThreadSafety.Helper SyncHelper = new ThreadSafety.Helper();
void Example() {

  // Method A:
  SyncHelper.Lock("[keyName]",()=>{ /*some code that needs to be thread safe*/ });

  // Method B: // Mirrors what happens internally in Method A.
  lock(SyncHelper["[keyName]"]) { /*some code that needs to be thread safe*/ }

  // Method C: (Conditional by key)
  SyncHelper.LockConditional("[keyName]",()=> {return property==null && !foo},()=>
  { /*some code that needs to be thread safe*/ });
}

只要您不尝试一次对流对象进行多次读写操作,优化文件访问就很简单:

ThreadSafety.File.Read(filePath,()=>{
  /* Some code that requires read access of the specified file.
     Allows for multiple readers. But blocks if there is a write lock in progress.
   */
});

ThreadSafety.File.Write(filePath,()=>{
  /* Some code that requires explicit write access of the specified file.
     Blocks all other access to the file until this is complete.
   */
});

我通常使用这些方法来等待文件访问,然后在一个 `Action` 中初始化一个流。

请记住,您需要在局部函数中应用 `while` / `try` / `catch` / `sleep` 重试策略来处理文件访问,以防其他进程在您的应用程序之外访问该文件。我本来可以将异常处理构建进去,但可能的实现方式过于多样,无法在此实用工具中真正做到健壮。在某些情况下,您可能正在写入文件并遇到 `IoException`,此时需要处理该错误并进行复杂的清理工作,然后才能继续。您可能不想重试。我包含了 `ThreadSafety.File.GetFileStreamForRead` 方法,该方法可以协助进行典型用法。

兴趣点 

我将 `LockCleanupDelay` 的设置留给您自行试验和调整。延迟清理似乎是避免文件访问冲突和担心过度锁定的理想解决方案。您可以将其设置为零,使其在每次运行后执行清理,但在我的测试中,这很容易导致 `IoException`。

重要的同步说明

无论是否使用 ThreadSafeHelper 工具,在实现条件锁(使用双重检查锁定模式)时,请务必不要在代码块结束之前修改用于条件的变量,否则它会在同步代码完成之前过早地使条件失效。

// Example A
object result;
ThreadSafety.LockConditional(dictionary,()=> !dictionary.TryGetValue(key, ref result),()=>
{
  object temp;
  /* some potentially complex code that may take some time to finish
     and eventually sets/initializes 'temp' */
  
  // Absolute last step:
  dictionary.Add(key,result = temp);
});

// Example B
ThreadSafety.LockConditional("[keyName]",()=> _value==null,()=>
{
  object result;
  /* some potentially complex code that may take some time to finish
     and eventually sets/initializes 'result' */
  
  // Absolute last steps:
  // System.Threading.Thread.MemoryBarrier(); // memory fence for multiprocessor systems.
  _value = result;
});

在上面的示例中,如果您出于任何原因在函数末尾以外的任何地方添加了该值,您可能会在该值准备好之前返回该值。

`Thread.MemoryBarrier()` 被建议用于避免处理器乱序问题。根据 MSDN 的说法:“它同步内存。实际上,它会刷新当前线程所执行处理器的缓存内存到主内存。” 但显然 `lock` 关键字(`Monitor.Enter` / `Exit`)隐式创建了一个完整的内存栅栏,因此不需要 `Thread.MemoryBarrier()`。

欢迎提出任何问题、评论、批评、建议和改进!

© . All rights reserved.