For years there has been an ever growing Elephant in the room for most programming languages; concurrency and multithreading. Most underlying languages implement the concept of threads allowing concurrent tasks however most languages did not implement, or implement an easy approach to share data between threads.
For concurrency there are really two types of data that get shared; mutable and immutable types – whether the data being shared changes or does not. Within .Net most types are mutable and thus straight away the programmer has to implement a locking or mutex based system to syncronise the data between threads.
Starting in .Net 4 Microsoft introduced the Task Parallel Library (TPL) which added various functionality as well to aid programmers easily create multithreaded (task based) applications via parallel loops and enumerators in the System.Threading.Tasks.Parallel namespace. This increased the pressure to performance tuned mutable and immutable data structures to surpass the now common List<T>.
With .Net 4 there was the introduction of Thread Safe collections under System.Collections.Concurrent which remove the need for developers to add their own locking code. These collections were designed for highly (Read Server Grade) level of concurrency and so for applications with a handful of threads, there would be a performance hit. All these collections were mutable and so there would be the issue of possible data corruption via user code and the need to resyncronise the data.
Immutable thread safe objects were sort after as they have the benefits of not requiring resyncronisation (the data does not change). A trade off is like when performing string additions, an addition to an immutable object requires the construction of a new object.
Under the hood the ImmutableList<T> as part of the Immutable Objects Collection uses a thinly wrapped AVL tree, thus modifications only require rebuilding the affect node and perhaps spine in Log(n) time. As nodes are forward linking there is no back traversal and thus back nodes can be rebuilt. This gives the performance benefits as multiple instances of an Immutablelist<T> can share much of the same commonality and thus have memory and garbage performance improvements over a programmer providing a lock around List<T> or providing a snapshot of a list via .Clone() or .ToList()
There is of course some performance hits when performing certain functions or programming in certain scenerios.
There are some noteworthy differences in algorithmic complexity between mutable and immutable collection types:
Mutable (amortized) | Mutable (worst case) | Immutable | |
---|---|---|---|
Stack.Push | O(1) | O(n) | O(1) |
Queue.Enqueue | O(1) | O(n) | O(1) |
List.Add | O(1) | O(n) | O(log n) |
HashSet.Add | O(1) | O(n) | O(log n) |
SortedSet.Add | O(log n) | O(n) | O(log n) |
Dictionary.Add | O(1) | O(n) | O(log n) |
SortedDictionary.Add | O(log n) | O(n log n) | O(log n) |
Due to the use of an AVL tree, traversal is done mostly in log(n) time, which should be considered for deeply nested data objects.
All of this is available via the NuGet package, which contains the following types:
- ImmutableStack<T>
- ImmutableQueue<T>
- ImmutableList<T>
- ImmutableHashSet<T>
- ImmutableSortedSet<T>
- ImmutableDictionary<K, V>
- ImmutableSortedDictionary<K, V>