Based on my understanding, given a C# array, the act of iterating over the array concurrently from multiple threads is a thread safe operation.
By iterating over the array I mean reading all the positions inside the array by means of a plain old for
loop. Each thread is simply reading the content of a memory location inside the array, no one is writing anything so all the threads read the same thing in a consistent manner.
This is a piece of code doing what I wrote above:
public class UselessService
{
private static readonly string[] Names = new [] { "bob", "alice" };
public List<int> DoSomethingUseless()
{
var temp = new List<int>();
for (int i = 0; i < Names.Length; i++)
{
temp.Add(Names[i].Length * 2);
}
return temp;
}
}
So, my understanding is that the method DoSomethingUseless
is thread safe and that there is no need to replace the string[]
with a thread safe type (like ImmutableArray<string>
for instance).
Am I correct ?
Now let's suppose that we have an instance of IEnumerable<T>
. We don't know what the underlying object is, we just know that we have an object implementing IEnumerable<T>
, so we are able to iterate over it by using the foreach
loop.
Based on my understanding, in this scenario there is no guarantee that iterating over this object from multiple threads concurrently is a thread safe operation. Put another way, it is entirely possible that iterating over the IEnumerable<T>
instance from different threads at the same time breaks the internal state of the object, so that it becomes corrupted.
Am I correct on this point ?
What about the IEnumerable<T>
implementation of the Array
class ? Is it thread safe ?
Put another way, is the following code thread safe ? (this is exactly the same code as above, but now the array is iterated by using a foreach
loop instead of a for
loop)
public class UselessService
{
private static readonly string[] Names = new [] { "bob", "alice" };
public List<int> DoSomethingUseless()
{
var temp = new List<int>();
foreach (var name in Names)
{
temp.Add(name.Length * 2);
}
return temp;
}
}
Is there any reference stating which IEnumerable<T>
implementations in the .NET base class library are
actually thread safe ?
Is iterating over an array with a for loop a thread safe operation in C# ?
If you're strictly talking about reading from multiple threads, that will be thread safe for Array
and List<T>
and just about every collection written by Microsoft, regardless of if you're using a for
or foreach
loop. Especially in the example you have:
var temp = new List<int>();
foreach (var name in Names)
{
temp.Add(name.Length * 2);
}
You can do that across as many threads as you want. They'll all read the same values from Names
happily.
If you write to it from another thread (this wasn't your question, but it's worth noting)
Iterating over an Array
or List<T>
with a for
loop, it'll just keep reading, and it'll happily read the changed values as you come across them.
Iterating with a foreach
loop, then it depends on the implementation. If a value in an Array
changes part way through a foreach
loop, it will just keep enumerating and give you the changed values.
With List<T>
, it depends what you consider "thread safe". If you are more concerned with reading accurate data, then it kind of is "safe" since it will throw an exception mid-enumeration and tell you that the collection changed. But if you consider throwing an exception to be not safe, then it's not safe.
But it's worth noting that this is a design decision in List<T>
, there is code that explicitly looks for changes and throws an exception. Design decisions brings us to the next point:
Can we assume that every collection that implements IEnumerable
is safe to read across multiple threads?
In most cases it will be, but thread-safe reading is not guaranteed. The reason is because every IEnumerable
requires an implementation of IEnumerator
, which decides how to traverse the items in the collection. And just like any class, you can do anything you want in there, including non-thread-safe things like:
You could even do something weird like make GetEnumerator()
return the same instance of your enumerator every time its called. That could really make for some unpredictable results.
I consider something to not be thread safe if it can result in unpredictable results. Any of those things could cause unpredictable results.
You can see the source code for the Enumerator
that List<T>
uses, so you can see that it doesn't do any of that weird stuff, which tells you that enumerating List<T>
from multiple threads is safe.
To assert that your code is thread-safe means that we must take your words for granted that there is no code inside the UselessService
that will try to replace concurrently the contents of the Names
array with something like "tom" and "jerry"
or (more sinister) null and null
. On the other hand using an ImmutableArray<string>
would guarantee that the code is thread-safe, and everybody could be assured about that just by looking the type of the static readonly field, without having to inspect carefully the rest of the code.
You may find interesting these comments from the source code of the ImmutableArray<T>
, regarding some implementation details of this struct:
A readonly array with O(1) indexable lookup time.
This type has a documented contract of being exactly one reference-type field in size. Our own
System.Collections.Immutable.ImmutableInterlocked
class depends on it, as well as others externally.IMPORTANT NOTICE FOR MAINTAINERS AND REVIEWERS:
This type should be thread-safe. As a struct, it cannot protect its own fields from being changed from one thread while its members are executing on other threads because structs can change in place simply by reassigning the field containing this struct. Therefore it is extremely important that Every member should only dereference
this
ONCE. If a member needs to reference the array field, that counts as a dereference ofthis
. Calling other instance members (properties or methods) also counts as dereferencingthis
. Any member that needs to usethis
more than once must instead assignthis
to a local variable and use that for the rest of the code instead. This effectively copies the one field in the struct to a local variable so that it is insulated from other threads.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With