SemaphoreSlim 和 Semaphore 可以限制同时访问资源的最大线程数量,而不是像 Monitor 一样只能限制一个线程。 SemaphoreSlim 比 Semaphore 更轻量,但仅限于单个进程。如果可能,您最好使用 SemaphoreSlim 而不是 Semaphore。
ReaderWriterLockSlim 可以区分两种对访问资源的方式。它允许无限数量的读取器 (readers) 同时访问资源,并且限制同时只允许一个写入器 (writers) 访问锁定资源。读取时线程安全,但修改数据时需要独占资源,很好地保护了资源。
AutoResetEvent、ManualResetEvent 和 ManualResetEventSlim 将堵塞传入的线程,直到它们接收到一个信号 (即调用 Set() )。然后等待中的线程将继续执行。AutoResetEvent 在下一次调用 Set() 之前,将一直阻塞,并只允许一个线程继续执行。ManualResetEvent 和 ManualResetEventSlim 不会堵塞线程,除非 Reset() 被调用。ManualResetEventSlim 比前两者更轻量,更值得推荐。
Interlocked 提供一种选择——原子操作,这是替代 locking 和其他同步基元更好的选择(如果适用):
// non-atomic operation with a lock lock (syncObject) { counter++; } // equivalent atomic operation that doesn't require a lock Interlocked.Increment(ref counter);
并发集合
当一个临界区需要确保对数据结构的原子访问时,用于并发访问的专用数据结构可能是更好和更有效的替代方案。例如,使用 ConcurrentDictionary 而不是 Dictionary,可以简化 lock 语句示例:
var counters = new ConcurrentDictionary< int, int >(); counters.TryAdd(key, 0); lock (syncObject) { counters[key]++; }
自然地,也有可能像下面一样:
counters.AddOrUpdate(key, 1, (oldKey, oldValue) => oldValue + 1);
因为 update 的委托是临界区外面的方法,因此,第二个线程可能在第一个线程更新值之前,读取到同样的旧值,使用自己的值有效地覆盖了第一个线程的更新值,这就丢失了一个增量。错误使用并发集合也是无法避免多线程带来的问题。 并发集合的另一个替代方案是 不变的集合 (immutable collections)。 类似于并发集合,同样是线程安全的,但是底层实现是不一样的。任何关改变数据结构的操作将不会改变原来的实例。相反,它们返回一个更改后的副本,并保持原始实例不变:
var original = new Dictionary< int, int >().ToImmutableDictionary(); var modified = original.Add(key, value);
因此在一个线程中对集合任何更改对于其他线程来说都是不可见的。因为它们仍然引用原来的未修改的集合,这就是不变的集合本质上是线程安全的原因。 当然,这使得它们对于解决不同集合的问题很有效。最好的情况是多个线程在同一个输入集合的情况下,独立地修改数据,在最后一步可能为所有线程合并变更。而使用常规集合,需要提前为每个线程创建集合的副本。
并行LINQ (PLINQ)
并行LINQ (PLINQ) 是 Task Parallel Library 的替代方案。顾名思义,它很大程度上依赖于 LINQ(语言集成查询)功能。对于在大集合中执行相同的昂贵操作的场景是很有用的。与所有操作都是顺序执行的普通 LINQ to Objects 不同的是,PLINQ可以在多个CPU上并行执行这些操作。 发挥优势所需要的代码改动也是极小的:
// sequential execution var sequential = Enumerable.Range(0, 40) .Select(n => ExpensiveOperation(n)) .ToArray(); // parallel execution var parallel = Enumerable.Range(0, 40) .AsParallel() .Select(n => ExpensiveOperation(n)) .ToArray();
如你所见,这两个代码片段的不同仅仅是调用 AsParallel()。这将IEnumerable 转换为 ParallelQuery,导致查询的部分并行运行。要切换为回顺序执行,您可以调用 AsSequential(),它将再次返回一个IEnumerable。 默认情况下,PLINQ 不保留集合中的顺序,以便让进程更有效率。但是当顺序很重要时,可以调用 AsOrdered():
var parallel = Enumerable.Range(0, 40) .AsParallel() .AsOrdered() .Select(n => ExpensiveOperation(n)) .ToArray();
同理,你可以通过调用 AsUnordered() 切换回来。
在完整的 .NET Framework 中并发编程
由于 .NET Core 是完整的 .NET Framework 的简化实现,所以 .NET Framework 中所有并行编程方法也可以在.NET Core 中使用。唯一的例外是不变的集合,它们不是完整的 .NET Framework 的组成部分。它们作为单独的 NuGet 软件包(System.Collections.Immutable)分发,您需要在项目中安装使用。
结论: