在 C# 中使用 Parallel.ForEach 方法时,如果你尝试在并行循环中对共享变量进行赋值,很可能会遇到线程安全问题或竞争条件(race conditions),这可能导致数据不一致、程序崩溃或其他不可预测的行为。
问题描述
假设你有以下代码:
int sharedVariable = 0;
Parallel.ForEach(someCollection, item =>
{
// 假设这里有一些计算
int result = ComputeSomething(item);
// 尝试更新共享变量
sharedVariable = result;
});
在这段代码中,sharedVariable 被多个线程同时访问和修改,这是不安全的。每次一个线程尝试写入 sharedVariable 时,它可能会覆盖其他线程之前的结果,或者由于处理器缓存和内存一致性问题,导致最终的值不正确。
解决方案
使用线程安全的集合或变量:
对于简单的整数或浮点数,可以使用 Interlocked 类来确保线程安全的读写操作。
对于更复杂的类型,可以考虑使用 Concurrent 命名空间下的集合,如 ConcurrentBag, ConcurrentQueue, ConcurrentDictionary<TKey, TValue> 等。
对于简单的累加操作,可以使用 Interlocked.Add 或 Interlocked.Increment。
使用局部变量并最后合并:
在每个线程中计算局部结果,然后在并行循环外部合并这些结果。
例如,使用局部变量并在最后合并:
List<int> localResults = new List<int>();
Parallel.ForEach(someCollection, item =>
{
int result = ComputeSomething(item);
localResults.Add(result);
});
int sharedVariable = localResults.Sum(); // 或者其他合并逻辑
使用自定义的线程安全数据结构:
如果内置的数据结构不满足需求,你可以实现自己的线程安全数据结构。
使用锁:
使用 lock 关键字可以确保只有一个线程在任何给定时间可以访问特定的代码块。但是,锁会降低并行性能,应谨慎使用。
例如,使用锁来保护共享变量:
object lockObj = new object();
int sharedVariable = 0;
Parallel.ForEach(someCollection, item =>
{
int result = ComputeSomething(item);
lock (lockObj)
{
sharedVariable = result; // 注意:这仍然只会保留最后一个线程的结果
}
});
注意:在上面的锁示例中,即使使用了锁,sharedVariable 仍然只会保留最后一个线程计算的结果,因为每次写入都会覆盖前一次的值。如果你需要累加或其他形式的合并,应考虑使用其他方法,如 Interlocked 或局部变量合并。
总之,处理并行编程中的共享资源时,需要特别小心以确保线程安全。选择正确的同步机制对于程序的正确性和性能至关重要。