Why does List.Add from multiple threads lead to varying results?

Multi tool use
Multi tool use
The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP


Why does List<>.Add from multiple threads lead to varying results?



I was wondering, if List<T> is thread-safe and read that several readers are no problem, but more than one writer may cause issues. So I wrote the following test to see what actually happens.


List<T>


[TestClass]
public class ListConcurrency
{
[TestMethod]
public void MultipleWritersTest()
{
var taskCnt = 10;
var addCnt = 100;

var list = new List<object>();
var tasks = new List<Task>();
for (int i = 0; i < taskCnt; i++)
{
var iq = i;
tasks.Add(Task.Run(() =>
{
Console.WriteLine("STARTING : " + iq);
for (int j = 0; j < addCnt; j++)
{
try
{
list.Add(new object());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("FINISHING: " + iq);

}));
}

Task.WhenAll(tasks).Wait();

Console.WriteLine("FINISHED: " + list.Count);
}
}



And here is an example output:


STARTING : 0
FINISHING: 0
STARTING : 1
FINISHING: 1
STARTING : 8
STARTING : 9
FINISHING: 9
FINISHING: 8
STARTING : 2
FINISHING: 2
STARTING : 7
STARTING : 3
FINISHING: 3
FINISHING: 7
STARTING : 4
FINISHING: 4
STARTING : 6
FINISHING: 6
STARTING : 5
FINISHING: 5
FINISHED: 979



I was surprised by two things:



If both would happen (expections and wrong item count) it would make sense... Is this simply the way List<T> demonstrates its non-thread-safety?


List<T>



EDIT: My opening line was badly phrased, I know that List<T> is not thread-safe (e.g. for iterating), but I wanted to see what happens, if it is "abused" in this way. As I wrote in a comment below, the results (that no exceptions will be thrown) may be useful for others when debugging.


List<T>





List<T> is not thread safe: list.Add(new object());
– Dmitry Bychenko
29 mins ago




List<T>


list.Add(new object());





The point of non-thread-safety is that it often leads to bugs that will not reproduce 100% of the time. I seem to remember seeing it sometimes manage to dereference nulls in the past and that would raise an exception. But what's the point of this test? You now know List<T> isn't thread safe, so why do any more work to "prove" it?
– Damien_The_Unbeliever
29 mins ago




List<T>





Where do you find that List is thread-safe? Documentation (bottom of page) states It is safe to perform multiple read operations on a List<T>, but issues can occur if the collection is modified while it’s being read. and that's what you are doing.
– Reniuz
24 mins ago




It is safe to perform multiple read operations on a List<T>, but issues can occur if the collection is modified while it’s being read.





Doing something that is not threadsafe is like driving through a red light at an intersection. Sometimes nothing happens at all. Sometimes there's an enormous crash and everyone dies. Sometimes other bad things happen in between those two extremes. The result is never guaranteed to be the same.
– J...
23 mins ago





And even if you found that it did throw exceptions (sometimes, which it may still do, you haven't proved that it won't), that information is pointless because it would be throwing exceptions due to programmer errors. You shouldn't catch those exceptions, you should fix your code.
– Damien_The_Unbeliever
9 mins ago






3 Answers
3



List<T> isn't threadsafe and you'll need to work with a different collection. You should try using ConcurrentBag<T> or any of the other collection types specified here in the documentation.


List<T>


ConcurrentBag<T>



Okay, let's look at Add and consider what happens when mutliple threads are accessing it:


Add


public void Add(T item) {
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}



Looks alright. But let's consider what happens for a particularly unlucky thread that has already gone past line 1 of this code before another thread manages to execute line 2, resulting in _size becoming equal to _items.Length. Our unlucky thread is now going to walk off the end of the _items array and throw an exception.


_size


_items.Length


_items



So, despite your "proof" that it won't throw an exception, I found an obvious race that would lead to one after about 2 minutes of inspecting the code.



If you check the source code of List you will see that internally it operates on array. The Add method expands array size and inserts new item:


// Adds the given object to the end of this list. The size of the list is
// increased by one. If required, the capacity of the list is doubled
// before adding the new element.
//
public void Add(T item)
{
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}



Now imagine you have array with size of 10 and 2 threads inserts at the same time - both expands array to 11 and one thread inserts at index 11 and other overwrites item at index 11. And that's why you get list count of 11 not 12.






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

DaAs0OerICOrW,GzdmE,cq97Ns7NOj59,h,If9lhHvGqlY
Lr,KmDP z pdSYVEx1mdc5A WJsh,iaosBdQDb7uqDLFQJ,Cz,vnWhcVKOhc KQIWa,pd W9

Popular posts from this blog

Makefile test if variable is not empty

Visual Studio Code: How to configure includePath for better IntelliSense results

Will Oldham