【问题标题】:Parallelised loop with adding to list添加到列表的并行循环
【发布时间】:2014-03-28 22:18:04
【问题描述】:

是否可以并行化一个循环长度增加的循环?

List<int> list = new List<int>() { 0, 1 };

for (int i = 0; i < list.Count; i++)
//Parallel.For(0, list.Count, (i) =>
{
    Console.WriteLine(list[i]);
    if (i == 0) list.Add(2);
}//);

//foreach (int i in list)
//Parallel.ForEach(list, (i) =>
//{
//    Console.WriteLine(i);
//    if (i == 0) list.Add(2);
//}//);

Console.ReadLine();

在这个简单的例子中,预期的输出是:

0
1
2

上面的代码与串行“for”一起工作正常,但由于集合被修改,串行“foreach”失败。对于这两种并行实现,代码都已完成,但输出缺少最终的“2”。

【问题讨论】:

  • 您可以将代码拆分为工作项的线程安全列表和一些可以多线程处理工作项的代码。使用“下一个”工作项的指针和一些锁定,你应该没问题。

标签: c# .net


【解决方案1】:

在 for each 循环中更改集合是无效的。基本上以任何方式修改列表都会使枚举数无效。以下是 IEnumerator 文档的引用:

只要集合保持不变,枚举数就保持有效。如果对集合进行了更改,例如添加、修改或删除元素,则枚举器将不可恢复地失效并且其行为未定义。

有关更多信息,请查看this post。至于并行实现:

  • Parallel.ForEach - 这受制于与每个标准相同的 IEnumerator 问题
  • Parallel.For - 这会将循环数作为常量传递给 for,而不是作为参考。这意味着当计数发生变化时,它不会改变循环的次数

更安全的模式是在调用并行实现之前添加、删除和修改列表元素。然后线程可以处理这些元素。如果无法做到这一点,则确定循环后您将拥有的元素数量,然后使用数组按索引存储/处理这些元素。最后将所有非空值拉回列表中。这样你就不必担心你的列表的线程安全(Insert 会推动其他元素使你的索引无效)。以下应该有效:

// EX: might be initialized with a call to the database: "COUNT(id)"
int expectedElements = 10;
if (myList.Count < expectedElements)
  for (var idx = myList.Count; idx <= expectedElements; idx++) myList.Add(null);

var elements = myList.ToArray();
System.Threading.Tasks.Parallel.For(0, expectedElements, (idx) =>
{
  // "remove" the element
  if (idx % 3 == 0) elements[idx] = null;

  // "modify" the element
  if (idx % 3 == 1) elements[idx] = DifferentElement(idx);

  // "add" an element
  if (idx % 3 == 2) elements[idx] = GetNewElement(idx);
});

// clear current list, add new elements, remove null values
myList.Clear();
myList.AddRange(elements);
myList.RemoveAll(item => item == null);

现在您可以根据需要“添加”、“删除”和“修改”,结果将返回列表中!

【讨论】:

    【解决方案2】:
    for (int i = 0; i < list.Count; i++) //list.Count will only checked at first call
    {
        Console.WriteLine(list[i]);
        if (i == 0) list.Add(2);
    }
    

    听起来你的 list.Count 会被问一个,然后它将被保存在内存中,在你的情况下,list.Count 将是 2 并且永远不会移动,所以你将打印 list[0] 然后 list[1]。
    您可能也对 lock 感兴趣:

    线程 A:

    lock (list) {
        foreach (Object obj in list) {
            obj.doSomething();
            if(meet_condition) list2.add(obj)
        }
    }
    

    其中 list2 是静态属性。

    线程 B:

    lock (list) {
      list.Remove(Element);
    }
    

    一旦一个线程锁定了列表,其他线程就等到它被释放才能使用它。 在不知道您想用它做什么的情况下,很难为您提供更多帮助。

    【讨论】:

    • 是的,看起来串行 for 循环每次通过都会重新评估,所以它看到 list.Count 增加了。显然,并行实现不会重新评估。有没有办法解决这个问题?
    • 如果您的目标是在控制台中打印数字,您可以覆盖列表中的 add 方法,而不是简单的添加到列表中,您可以添加 Console.WriteLine(value) 然后 list.Add(value) ;
    • 这是我面临的问题的一个简单示例。实际的算法要复杂得多。
    • 也许您可以创建您的列表的临时副本并遍历这个复制的列表
    • 小心lock - 如果使用过多,它基本上会完全否定线程的价值。
    猜你喜欢
    • 1970-01-01
    • 2014-01-22
    • 2013-11-30
    • 1970-01-01
    • 2019-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多