Parallel.For 和 ConcurrentBag 给出不可预测的行为

Posted

技术标签:

【中文标题】Parallel.For 和 ConcurrentBag 给出不可预测的行为【英文标题】:Parallel.For and ConcurrentBag giving unpredictable behavior 【发布时间】:2020-02-25 00:17:21 【问题描述】:

我正在编写一个应用程序,它在某些时候需要一个网格体并计算邻接索引。为此,我定义了一个 ConcurrentBag 对象数组,然后,在一个并行的 for 循环中,我只检查一些面,如果它们有任何邻接关系,我将索引添加到相应索引中的所述包中。即:

private bool parallelize = true;
private volatile ConcurrentBag<int>[] edge_adjacencies;
if (parallelize)

    ...
    Parallel.For(0, face_count, compute_adjacency_single);
    ...


private void compute_adjacency_single(int cur_idx)

    edge_adjacencies[cur_idx] = new ConcurrentBag<int>();
    foreach(int test_idx in SOME_TEST_SPACE)
    
        if (test_idx != cur_idx)
        
            bool edge_adj, vertex_adj;
            get_adjacency(cur_idx, test_idx, out edge_adj, out vertex_adj);
            if (edge_adj && !collection_contains(edge_adjacencies[cur_idx], test_idx))
            
                edge_adjacencies[cur_idx].Add(test_idx);
            
        
    

然后我对集合进行索引并检查每个集合的大小是否为 3(它们的大小都应该正好为 3):

//DEBUGGING
for (int i = 0; i < face_count; i++)

    ConcurrentBag<int> cur = edge_adjacencies[i];
    if (cur.Count != 3) Console.WriteLine("incorrect:" + i);

//DEBUGGING

这个过程的结果是不可预测的:有时我根本没有输出(所有的大小都是 3),有时我得到不正确的输出:

运行 1:

incorrect:3791
incorrect:3792
incorrect:3829
incorrect:3837
incorrect:4476

运行 5:

incorrect:2855
incorrect:2856
incorrect:2879
incorrect:2880

运行 8:

incorrect:3271

每运行 9 次左右就会给出不正确的结果。

作为参考,当我串行运行时,它每次都能完美运行。

我阅读了 MS 文档,它确实说 System.Collections.Concurrent 中的集合应该是线程安全的,但似乎情况并非如此。

为什么会发生这种情况,是否有防止这种情况的好方法?

【问题讨论】:

如果没有两个线程访问数组中的同一个索引,为什么还要使用ConcurrentBag?有哪些并发访问? collection_contains 是如何实现的? 【参考方案1】:

嗯。这是一个猜测——但我不认为在compute_adjacency_single 中这样做edge_adjacencies[cur_idx] = .. 是线程安全的。

CuncurrentBag&lt;int&gt; 是非常线程安全的,但保存它们的实例的 array 不是。

我会使用 ConcurrentDictonary 而不是数组。

【讨论】:

我会看看是否可以使用字典来实现,尽管每个线程都访问该数组中的唯一元素... 它必须是一个线程安全的集合类型,一个“普通”的字典也会有同样的问题。问题不在于cur_idx 的唯一性,而在于(我怀疑)数组索引方法的实现。 如果每个线程访问数组中的不同索引,则应该没有线程问题。 @Rotem 这与您所说的问题不同。访问数组中的包 - 功能类似于 get 方法。该 get 方法返回特定索引处的包。该 get 方法不是线程安全的。 这正是我所说的。假设没有线程访问数组中的相同索引,则访问是安全的。【参考方案2】:

我会使用一个锯齿状数组来确定(或者一个扁平的一维数组,如果你不觉得它很难实现的话)但这只是我的偏好,因为我得到的数据或多或少是按索引排序的。

我认为问题在于使用bool edge_adj, vertex_adj; 时存在竞争条件。创建一个数组或任何您更喜欢的线程安全数据结构,并用get_adjacency 函数填充它,这样您就可以访问布尔值,而线程不会覆盖彼此的值。

【讨论】:

如果线程从不访问相同的数组元素,它们将如何覆盖彼此的值? @Rotem 因为没有数组!线程试图同时访问bool edge_adj, vertex_adj; 变量 这怎么可能?这些是本地变量。它们只存在于线程本地 foreach 循环中。 因为它们是通过引用get_adjacency 方法传递的,所以相同的地址被覆盖了。 但是地址是一个局部变量。每个线程的地址都不同。甚至可能在每个线程中循环的不同迭代中。

以上是关于Parallel.For 和 ConcurrentBag 给出不可预测的行为的主要内容,如果未能解决你的问题,请参考以下文章

Parallel.For循环清理[关闭]

Parallel类

C#的并发循环(for,foreach,parallel.for,parallel.foreach)对比

Parallel.for 循环执行

在Parallel.For中,是否可以同步每个线程?

Parallel.For没有给出一致的结果[重复]