Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parallelised loop with adding to list

Tags:

c#

.net

Is it possible to parallelise a loop where the length of the loop is increased within?

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();

In this simple example, the intended output is:

0
1
2

The above code works correctly with a serial 'for', but fails for the serial 'foreach' due to the collection being modified. For both parallelised implementations, the code completes but the output is missing the final '2'.

like image 287
user1765603 Avatar asked Mar 16 '26 17:03

user1765603


1 Answers

It isn't valid to change a collection in a for each loop. Basically modifying the list in any way invalidates the enumerator. The following is a quote from the documents for IEnumerator:

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined.

For more information take a look at this post. As far as the parallel implementations:

  • Parallel.ForEach - This is subject to the same IEnumerator issues that a standard for each is
  • Parallel.For - This passes the number of loops into the for as a constant, not as a reference. That means when count changes it doesn't change the number of times it will loop

A safer pattern is to add, remove, and modify your list elements prior to calling the parallels implementation. Then threads can process these elements. If that can't be done, then determine the number of elements that you will have after the loop, then use an array to store/process these by index. Finally pull any non-null values back into a list. That way you don't have to worry about thread safety with regards to your list (Insert would push other elements forward making your indexes invalid). The following should work:

// 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);

Now you can "add", "remove" and "modify" as much as you want and the result is back in a list!

like image 57
drew_w Avatar answered Mar 18 '26 05:03

drew_w