Thread safe collection

>>Return to C ා concurrent programming

1. introduction

  • Immutable set
    • Immutable collections usually share most of the storage space, so the waste is not large
    • Because it cannot be modified, it is thread safe
  • Thread safe collection
    • A mutable set that can be modified by multiple threads at the same time
      • Thread safety collections use a mixture of fine-grained locking and nonlocking techniques to ensure that threads are blocked for the shortest time
        • Usually it's not blocked at all
    • When enumerating many thread safe collections, a snapshot of the collection is created internally, and the snapshot is enumerated.
    • The main advantage of thread safe collection is that multiple threads can access it safely, and the code will only be blocked for a short time or not at all.

The following describes the commonly used immutable and thread safe collections with specific data structures.

2. Immutable stack and queue

Immutable sets use the pattern of returning a modified set, and the original set reference is immutable.

  • This means that if an instance of a particular immutable collection is referenced, it will not change.
var stack = ImmutableStack<int>.Empty;
stack = stack.Push(13);
var biggerStack = stack.Push(7);
// "7" is displayed first, then "13". 
foreach (var item in biggerStack)
    Console.WriteLine($"biggerStack {item}");

// Only "13" is displayed.
foreach (var item in stack)
    Console.WriteLine($"stack {item}");


biggerStack 7
biggerStack 13
stack 13

The two stacks actually share the memory of storage item 13 internally.

  • This implementation is very efficient and can easily create a snapshot of the current state
  • Every instance of an immutable collection is absolutely thread safe

ImmutableQueue is used in a similar way.

  • An instance of an immutable set is never changed.
  • Because it will not change, it is absolutely thread safe.
  • When a modified method is used for an immutable set, the modified set is returned.
  • Immutable set is very suitable for sharing state, but it is not suitable for the channel of exchanging data.

3. Immutable list

The inner part of the immutable list is organized by a binary tree. This is done to maximize the memory shared between instances of immutable lists.

  • This results in performance differences between immutablelist < T > and list < T > in common operations (see the table below).
Operation List<T> ImmutableList<T>
Add Sharing O(1) O(log N)
Insert O(N) O(log N)
RemoveAt O(N) O(log N)
Item[index] O(1) O(log N)

Immutable lists can use index to get data items, but you need to pay attention to performance issues. You cannot simply replace list < T > with it.

  • This means you should try to use foreach instead of for

4. Immutable Set set

  • ImmutableHashSet<T>
    • Is a collection without repeating elements
  • ImmutableSortedSet<T>
    • Is a sorted collection without duplicate elements
  • All have similar interfaces
var hashSet = ImmutableHashSet<int>.Empty;
hashSet = hashSet.Add(13);
hashSet = hashSet.Add(7);
// "7" and "13" are displayed in uncertain order. 
foreach (var item in hashSet)
    Console.Write(item + " ");

hashSet = hashSet.Remove(7);

var sortedSet = ImmutableSortedSet<int>.Empty;
sortedSet = sortedSet.Add(13);
sortedSet = sortedSet.Add(7);
// "7" is displayed first, then "13". 
foreach (var item in sortedSet)
    Console.Write(item + " ");

var smallestItem = sortedSet[0];
// smallestItem == 7
sortedSet = sortedSet.Remove(7);


7 13 
7 13 
Operation ImmutableHashSet<T> ImmutableSortedSet<T>
Add O(log N) O(log N)
Remove O(log N) O(log N)
Item[index] Unavailable O(log N)

The time complexity of the ImmutableSortedSet index operation is O(log N), not O(1), which is similar to the situation of immutablelist < T > in the last section.

  • This means that they apply the same warning: when using immutablesortedset < T >, try to use foreach instead of for.

You can quickly build it in a variable way and then convert it to an immutable set.

5. Immutable dictionary

  • ImmutableDictionary<TKey,TValue>
  • ImmutableSortedDictionar y<TKey,TValue>
var dictionary = ImmutableDictionary<int, string>.Empty;
dictionary = dictionary.Add(10, "Ten");
dictionary = dictionary.Add(21, "Twenty-One");
dictionary = dictionary.SetItem(10, "Diez");
// "10Diez" and "21twenty one" are displayed, and the order is uncertain. 
foreach (var item in dictionary)
    Console.WriteLine(item.Key + ":" + item.Value);

var ten = dictionary[10]; // ten == "Diez"
dictionary = dictionary.Remove(21);

var sortedDictionary = ImmutableSortedDictionary<int, string>.Empty; sortedDictionary = sortedDictionary.Add(10, "Ten");
sortedDictionary = sortedDictionary.Add(21, "Twenty-One");
sortedDictionary = sortedDictionary.SetItem(10, "Diez");
// "10Diez" is displayed first, followed by "21twenty one". 
foreach (var item in sortedDictionary)
    Console.WriteLine(item.Key + ":" + item.Value);

ten = sortedDictionary[10];
// ten == "Diez"
sortedDictionary = sortedDictionary.Remove(21);


 Operation I
Operation ImmutableDictionary<TK,TV> ImmutableSortedDictionary<TK,TV>
Add O(log N) O(log N)
SetItem O(log N) O(log N)
Item[key] O(log N) O(log N)
Remove O(log N) O(log N)

6. Thread safety dictionary

var dictionary = new ConcurrentDictionary<int, string>(); 
var newValue = dictionary.AddOrUpdate(0,
key => "Zero",
(key, oldValue) => "Zero");

The AddOrUpdate method is somewhat complex because it must perform multiple steps, depending on the current content of the concurrent dictionary.

  • The first argument to the method is the key
  • The second parameter is a delegate that converts the key (0 in this case) to the value added to the dictionary (Zero in this case)
    • The delegate runs only if the key is not in the dictionary.
  • The third parameter is also a delegate, which converts the key (0) and the original value to the modified value in the dictionary
    • The delegate runs only if the key already exists in the dictionary.
  • The new value corresponding to the key AddOrUpdate return (the same as the value returned by one of the delegates).

AddOrUpdate may call one (or both) of the delegates more than once. This is rare, but it does happen.

  • So these delegates have to be simple, fast, and have no side effects
  • These delegates can only create new values and cannot modify other variables in the program
  • This principle applies to all delegates used by methods with concurrentdictionary < tkey, tvalue >
// Use the same dictionary as before.
string currentValue;
bool keyExists = dictionary.TryGetValue(0, out currentValue);

// Use the same dictionary as before.
string removedValue;
bool keyExisted = dictionary.TryRemove(0, out removedValue);
  • If multiple threads read and write a shared collection, tvalue > is the most appropriate way to use concurrent directory < tkey
  • If you don't make frequent changes (few changes), it's better to use immutabledictionary < tkey, tvalue >.

  • If some threads only add elements and others only remove elements, it is best to use the producer / consumer collection.

7. Blocking queue

  • GetConsumingEnumerable blocks threads
  • After the CommpleteAdding method is executed, all threads blocked by GetConsumingEnumerable begin to execute
  • Each element will only be consumed once
private static readonly BlockingCollection<int> _blockingQueue = new BlockingCollection<int>();
public static async Task BlockingCollectionSP()
    Action consumerAction = () =>
        Console.WriteLine($"started print({Thread.CurrentThread.ManagedThreadId}).");
        // Display "7" first, then "13".
        foreach (var item in _blockingQueue.GetConsumingEnumerable())
             Console.WriteLine($"print({Thread.CurrentThread.ManagedThreadId}) {item}");
        Console.WriteLine($"ended print({Thread.CurrentThread.ManagedThreadId}).");
    Task task1 = Task.Run(consumerAction);
    Task task2 = Task.Run(consumerAction);
    Task task3 = Task.Run(consumerAction);

    System.Console.WriteLine($"added 7.");
    System.Console.WriteLine($"added 13.");

    catch (Exception ex)

    await Task.WhenAll(task1, task2, task3);


started print(4).
started print(3).
started print(6).
added 7.
added 13.
ended print(6).
InvalidOperationException:The collection has been marked as complete with regards to additions.
print(4) 7
ended print(4).
print(3) 13
ended print(3).

8. Blocking stack and package

  • By default, blockingcollection < T > in. NET is used as a blocking queue, but it can also be used as a producer / consumer collection of any type.
  • Blockingcollection < T > actually encapsulates thread safe collection and implements iproducerconsumercollection < T > interface.
    • Therefore, rules can be specified when creating an instance of blockingcollection < T >
BlockingCollection<int> _blockingStack = new BlockingCollection<int>( new ConcurrentStack<int>());
BlockingCollection<int> _blockingBag = new BlockingCollection<int>( new ConcurrentBag<int>());

Replace to Blocking queue Try in the sample code.

9. Asynchronous queue

public static async Task BufferBlockPS()
    BufferBlock<int> _asyncQueue = new BufferBlock<int>();
    Func<Task> concurrentConsumerAction = async () =>
         while (true)
             int item;
                 item = await _asyncQueue.ReceiveAsync();
             catch (InvalidOperationException)
             Console.WriteLine($"print({Thread.CurrentThread.ManagedThreadId}) {item}");
    Func<Task> consumerAction = async () =>
            // Display "7" first, then "13". Single thread available
            while (await _asyncQueue.OutputAvailableAsync())
                Console.WriteLine($"print({Thread.CurrentThread.ManagedThreadId}) {await _asyncQueue.ReceiveAsync()}");
        catch (Exception ex)


    Task t1 = consumerAction();
    Task t2 = consumerAction();

    // Task t1 = concurrentConsumerAction();
    // Task t2 = concurrentConsumerAction();

    // Producer code
    await _asyncQueue.SendAsync(7);
    await _asyncQueue.SendAsync(13);
    await _asyncQueue.SendAsync(15);
    System.Console.WriteLine("Added 7 13 15.");

    await Task.WhenAll(t1, t2);


Added 7 13 15.
print(4) 7
print(6) 13
print(4) 15
InvalidOperationException(3):The source completed without providing data to receive.

10. Asynchronous stack and package

Nito.AsyncEx Library

AsyncCollection<int> _asyncStack = new AsyncCollection<int>( new ConcurrentStack<int>());
AsyncCollection<int> _asyncBag = new AsyncCollection<int>( new ConcurrentBag<int>());

11. Blocking / asynchronous queue

Bufferblock < T > has been introduced in blocking queue

Here's actionblock < int >

public static async Task ActionBlockPS()
    ActionBlock<int> queue = new ActionBlock<int>(u => Console.WriteLine($"print({Thread.CurrentThread.ManagedThreadId}) {u}"));

    // Asynchronous producer code
    await queue.SendAsync(7);
    await queue.SendAsync(13);
    System.Console.WriteLine("Added async.");
    // Synchronized producer code 
    System.Console.WriteLine("Added sync.");


Added async.
Added sync.
print(3) 7
print(3) 13
print(3) 15
print(3) 17

Tags: C# snapshot Programming

Posted on Sat, 01 Feb 2020 00:51:13 -0500 by steve m