更新 Parallel.ForEach 线程中对象的属性是不是安全?

Posted

技术标签:

【中文标题】更新 Parallel.ForEach 线程中对象的属性是不是安全?【英文标题】:Is updating a property for an object in a Parallel.ForEach thread safe?更新 Parallel.ForEach 线程中对象的属性是否安全? 【发布时间】:2021-05-27 13:14:22 【问题描述】:
Public Class Customer

   public int CustomerIdget;set;
   Public string CustomerNameget;set;


public class RunParallel(List<Customer> namelessCustomers)

   Parallel.ForEach(namelessCustomers, (customer) =>
      var customerName = CallAPIToReturnCustomerName(customer.CustomerId);
      customer.CustomerName = customerName
   

您好,customer.CustomerName=customerName 线程安全。我想要的是更新客户列表,以便每个对象都获得客户名称。 如果不是,我怎样才能让这样的事情起作用。你能解释一下为什么这不是线程安全的吗?

【问题讨论】:

每个并行迭代似乎都在不同的Customer 实例上工作,那么为什么这不是线程安全的呢? (假设CallApiToReturnCustomerName 是线程安全的) 对不起,我不擅长多线程。 【参考方案1】:

首先,字符串不可变的,而引用是原子的,所以这个属性是线程安全的,并不是说它不会过时价值观和数据竞争——尽管这里似乎并非如此

其次,您在parallel.ForEach 中调用IO 工作负载,这不是最佳的。这意味着,您正在阻塞和捆绑 线程池 线程,这些线程可以有效地卸载到 IO 完成端口。您最好让 API 调用为async,并使用Task.WhenAll

示例

public async Task<Customer> CallAPIToReturnCustomerNameAsync(int customerId) 

   ///await someThingAsyncHere();


...

async Task Process(Customer customer)

    customer.CustomerName = await CallAPIToReturnCustomerNameAsync(customer.CustomerId);


var tasks = namelessCustomers.Select(Process);
await Task.WhenAll(tasks);

【讨论】:

【参考方案2】:

主要的危险是并发读/写或写/写。如果保证每个对象只能从单个线程访问,则默认情况下这将是线程安全的,即该列表没有同一客户的多个副本,并且在循环运行时没有其他线程可以访问该客户列表。

即使有并发访问,只要类型是引用类型,或者值类型小于本机整数,这也是原子的,如本例中的情况。但是,这仅适用于单个读/写访问,任何涉及多个操作的操作都是不安全的。

【讨论】:

【参考方案3】:

线程安全和并发是两种在两种情况下值得注意的方法: 1-并发读取器/写入器可以进入一个关键区域。 (即使在并发编程或并行性中)并发编程是指多线程,并行性是指多核编程。 2-作家引起读者,反之亦然。例如,在单个区域上写入之前,您必须阅读和检查。 上述两条规则都可能导致竞态条件,应该值得注意。 在你的情况下:

您没有任何种族,因为您只是在写作,顺序并不重要。 您不再有任何比赛,因为您在多个领域写作,并且领域(不同客户的财产)不共享。

注意:竞态条件与多 X 编程的方式/堆栈/语言无关,您可能在多线程、多核编程中都有这种情况。因此,我强烈建议您仔细检查您的代码并找到关键区域并写入/写入或写入/读取时刻。你会发现没有:)

【讨论】:

以上是关于更新 Parallel.ForEach 线程中对象的属性是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

计算 Parallel.ForEach 使用的线程数

何时使用 Parallel.ForEach,何时使用 PLINQ

在 parallel.ForEach 循环中获取线程 ID

.Net 中的多个 Parallel.ForEach 循环

何时使用 Parallel.ForEach,何时使用 PLINQ

强制终止由 Parallel.ForEach 生成的线程 [重复]