9

I know the C# System.Collections.Generic.List object is not thread safe. But I am wondering why this piece of code generates null values.

Task.Run(() =>
{
    for (var i = 0; i < 10; i++)
    {
        var str = $"Test {i}";
        list.Add(str);

        if (i == 9)
        {
            i = 0;
        }
    }
});

Task.Run(() =>
{
    while (true)
    {
        list.Remove("Test 1");
        list.Remove("Test 2");
        list.Remove("Test 3");
        list.Remove("Test 4");
        list.Remove("Test 5");
        list.Remove("Test 6");
        list.Remove("Test 7");
        list.Remove("Test 8");
        list.Remove("Test 9");
    }
});

This is a part of the list after some seconds:

enter image description here

The thread which is responsible to remove the entries from the list can crash, if the entry is not present in the list. Therefore and for other multithreading reasons I understand why some objects are not removed from the list, but I do not understand how these null values are generated. Has anyone an explanation how these values are generated?

4
  • 5
    List<T> is not thread-safe: it says so right here. You cannot safely read/write to it on multiple threads at the same time. Any number of weird things can happen, including the things you're seeing.
    – canton7
    Commented Feb 25, 2020 at 14:31
  • 1
    You may have caught the list while it's resizing its internal buffer and copying the new data over. The List knows it's not supposed to be called from multiple threads, so it uses simple array indexes. One of your calls may be looking at the old, one at the new buffer before the data was copied over, one at the new buffer with the data. That could produce nulls and duplicates Commented Feb 25, 2020 at 14:34
  • Threading is difficult, and it is recommended to avoid it if you haven't studied at least the basics of it. If you have time for reading, here is a great resource: Threading in C#. Commented Feb 25, 2020 at 16:29

2 Answers 2

5

in addition to @Marc Gravell answer.

but I do not understand how these null values are generated

The first thread continuously adds "Test {i}". However, the next thread removes "Test {i}". Therefore, Those null values are as a result of removal action of the next thread.

of worth to say that, next "Test {i}" won't be replaced to removed one but appended at the end of the generic collection.

The final result would behave like below:

Test 1 null Test 3 null null Test 6 .... Test 1 Test 2 null null Test 6 ...  
1
  • 3
    This is the answer I was looking for. The add allocates the next part of memory while the remove (to the nearly same time) clears the memory before which never gets overwritten again. Thanks! Commented Feb 25, 2020 at 14:51
5

List<T> is not thread-safe except for N reads and zero writes; any non-zero number of writes alongside anything else is not supported, and such behavior is completely undefined. If you need concurrency: either add synchronization, or use a concurrent collection type.

6
  • what do you mean add synchronization? How would you add it?
    – OlegI
    Commented Feb 25, 2020 at 14:32
  • 1
    @OlegI use a lock statement. Or better yet, don't use a List, use a concurrent collection instead. Commented Feb 25, 2020 at 14:35
  • @PanagiotisKanavos ah I see, I thought I am missing something. Thank you
    – OlegI
    Commented Feb 25, 2020 at 14:36
  • @PanagiotisKanavos, Using a thread-safe alternative to List might or might not be appropriate. It depends on the programmer's expectations of what the collection contains at any given moment in time and, on how the contents might relate to other data in the program. Even if you build a thing entirely out of thread-safe objects, that does not guarantee that the thing you built will be thread safe. Commented Feb 25, 2020 at 17:36
  • @SolomonSlow which isn't what Oledl asked Commented Feb 25, 2020 at 20:40

Not the answer you're looking for? Browse other questions tagged or ask your own question.