A few points about concurrency

My recent work assignment required me to quickly brush up on my rusty concurrent programming knowledge. I have some thoughts on this topic, and even though they might seem disconnected, I’m sharing them in this post.

While a grasp of concurrency fundamentals (threads, locks, etc.) is crucial for all software developers, especially in our multi-core world, I believe it’s becoming less common. As we spend more time coding in languages like Javascript, which abstracts away thread management, our exposure to concurrency concepts diminishes. We might be living in a time of concurrency-ready hardware but with concurrency-unready developers.

A Multi-Core Landscape

The widespread adoption of multi-core processors not only presents more opportunities to leverage multithreading (especially in CPU-intensive scenarios) but also introduces new factors to consider:

  • Spinlocks: Spinlocks I was completely oblivious to this synchronization mechanism until I discovered it had been added to .Net 4.0.

    In software engineering, a spinlock is a lock that forces a thread attempting to acquire it to wait in a loop (“spin”), continuously checking for the lock’s availability. As the thread remains active without performing productive work, this lock mechanism represents a form of busy waiting.

    The concept of a thread busy-waiting to prevent a context switch only makes sense in multi-core or multi-processor environments. Here’s an example: Thread1 acquires a spinlock and is descheduled before completion. Thread2 then attempts to acquire the same lock, entering a busy-wait state. With only one core available, and Thread2 consuming it, Thread1 cannot be rescheduled to finish its operation and release the lock until Thread2’s quantum expires, triggering a context switch. This scenario renders the spinlock counterproductive. You can verify my explanation here.

  • Simultaneous Instruction Execution: Remember that identical instructions can execute concurrently on different cores. Recently, I encountered a bug where a pseudo-random string generator (using the date down to the millisecond - yyyyMMddHHmmssFFF - for a named pipe) caused issues. Under stress tests, threads occasionally failed to create the named pipe because another thread had already created it. This means both threads executed the MyDateToStringFormatter(DateTime.Now); instruction on separate cores during the same millisecond!

Atomicity, Thread Safety, and Concurrent Collections

Another crucial aspect in multithreaded programming is operation atomicity. For instance, in a multithreaded context where multiple threads read and write to a standard Dictionary, this code snippet is unsafe:

1
2
if(myDictionary.ContainsKey("myKey"))
myVar = myDictionary["myKey"];

The first instruction might return true, but another thread could remove the key before the second instruction executes (even more likely in multi-core systems).

Fortunately, .NET 4 introduced a suite of thread-safe collections, Concurrent Collections, allowing us to easily rectify the previous code using a ConcurrentDictionary:

1
myDictionary.TryGetValue("myKey", out myVar);

However, some cases are less obvious. Consider adding elements concurrently to a standard List using myList.Add(item). While seemingly harmless, this Add call is not atomic. Adding an element involves checking and potentially resizing the list. Thread1 might resize the list, and before it sets the new size, Thread2 could execute its own Add, initiating another resize. This StackOverflow post illustrates the issue: common question.

You might consider using a ConcurrentList but discover it doesn’t exist. This makes sense because such a collection has limited utility. [good discussion here]. When multiple threads read and write to a list concurrently, accessing elements by index becomes unreliable. What you insert at index 3 might end up at index 5 due to other threads’ operations. Instead, you might need a ConcurrentQueue, ConcurrentStack, or a container for thread-safe item insertion/removal with LINQ to Objects support. A quick glance at the System.Collections.Concurrent namespace provides a solution: ConcurrentBag. Although optimized for single-thread production and consumption, it functions perfectly in other concurrent scenarios (read more here and here).

While enumerating a ConcurrentCollection is safe (concurrent modifications won’t throw an InvalidOperationException), there’s an intriguing difference in how ConcurrentDictionary and ConcurrentBag handle enumeration:

  • ConcurrentDictionary enumerates the “live” dictionary:

    The enumerator is safe for concurrent use with reads and writes, but it doesn’t represent a snapshot. Enumerated content may include modifications after calling GetEnumerator.

  • ConcurrentBag enumerates a snapshot:

    The enumeration is a point-in-time snapshot, unaffected by subsequent updates. It’s safe for concurrent use with bag reads and writes.

Regarding atomicity, remember that it extends beyond single or double C#/Java instructions to the machine level. This is where the Interlocked class comes in, interlocked, offering methods like Read for safely reading 64-bit values on 32-bit systems. Without it, one thread might read the first 32 bits while another overwrites the second 32 bits before the first thread finishes, resulting in a corrupted value.

here

Licensed under CC BY-NC-SA 4.0
Last updated on Nov 10, 2023 07:17 +0100