使用并行化循环将元素添加到列表中

3

是否有可能并行化一个循环,其中循环的长度在内部增加?

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”。

1
你可以将代码拆分为一个线程安全的工作项列表和一些可多线程处理工作项的代码。使用“下一个”工作项的指针和一些锁定即可。 - CodingBarfield
2个回答

3
在"for each循环"中修改集合是不被允许的。基本上,以任何方式修改列表都会使枚举器无效。以下是来自IEnumerator文档的引用:
“只要集合保持不变,枚举器就保持有效。如果对集合进行更改,例如添加、修改或删除元素,则枚举器将不可恢复地失效,并且其行为未定义。”
有关更多信息,请参阅本帖。至于并行实现:
- Parallel.ForEach - 这受到与标准for each相同的IEnumerator问题的影响。 - Parallel.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);

现在您可以随意“添加”、“删除”和“修改”,结果将返回到列表中!

0
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循环在每次遍历时都会重新计算,因此它会看到列表数量已经增加。显然,并行实现不会重新计算。有没有解决方法? - user1765603
如果你的目标是在控制台中打印数字,你可以重写列表中的add方法。与简单地将值添加到列表不同,你可以添加Console.WriteLine(value),然后再添加到列表中(list.Add(value))。 - Anthony Raymond
这是我面临的问题的一个简单例子。实际算法要复杂得多。 - user1765603
也许你可以创建一个临时副本的列表,并遍历这个复制的列表。 - Anthony Raymond
小心使用“lock” - 如果过度使用,它基本上会抵消线程的价值。 - drew_w

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接