Following up on the Kotlin Coroutines Basic Concepts post, in this article, I will leverage those concepts to address a classic concurrency problem: Race Conditions, using the Map-Reduce pattern. Although concise, this post covers two fascinating topics—Race Conditions and Map-Reduce—so make sure to read through to the end! ;))
Table of Contents:
- What is a Race Condition?
- How to resolve Race Conditions?
- What is Map-Reduce?
- Resolve Race Conditions with Map-Reduce
1. What is a Race Condition?
Take a look at the following GIF:
and imagine the dogs arethreads while the ball is a shared resource ;))Without needing extensive explanations, you can easily see that the shared resource can become inconsistent or corrupted if multiple threads contend for it simultaneously.
2. How to resolve Race Conditions?
The root cause of a Race Condition is contention over a shared resource. Solving contention eliminates the race condition. There are two primary schools of thought:
1. Using Synchronizations[1] or Atomic Operations[2]
2. Avoiding shared state
Returning to the dogs and ball analogy, the two solutions translate to:
- Keep only 1 ball and force each dog to wait for its turn—the dogs won’t be very happy :joy:.
- Provide multiple balls so each dog gets its own—everyone is thrilled :muscle:.
→ The choice depends on your system requirements and constraints ;))
In the past, when multi-core CPUs were rare and RAM was scarce, #1 was the common choice. Today, with abundant multi-core hardware, I believe #2 is often the far superior and scalable architecture.
However, certain use cases still strictly require #1, such as bank account balance updates, inventory counters, or stock order books.
3. What is Map-Reduce?
By now, you likely recognize that #2 is the preferred way to mitigate Race Conditions in high-throughput modern systems. But how do we implement it? One prominent technique is Map-Reduce. So what is Map-Reduce?
Map-Reduce was originally introduced by Jeffrey Dean and Sanjay Ghemawat at Google in 2004, serving as the foundational concept behind Hadoop. At its core, it embodies the divide-and-conquer strategy through two primary phases:
- Map: Partition the input data into smaller chunks and process them independently in parallel.
- Reduce: Aggregate and consolidate the intermediate results to produce the final outcome.
To visualize the workflow:

This is the classic “Word Count” example from Hadoop MapReduce deep diving and tuning. The goal is counting word occurrences from an input stream. The Map stage splits and maps inputs, while the Reduce stage shuffles and reduces them into total counts.
4. Resolve Race Conditions with Map-Reduce
Now for the main practical demonstration. We will implement the classic Race Condition benchmark of incrementing a counter using 3 approaches: synchronous single-thread, Map-Reduce with Coroutines, and multi-threading with AtomicLong.
| |
The output benchmark looks like this:
increaseSync: counterSync [1000000000] - time [304001418]
increaseSync with Map-Reduce: counterMapReduce [1000000000] - time [171316295]
increaseWithAtomic: atomicCounter [1000000000] - time [19268418252]
Looking at the execution duration, increaseSync with Map-Reduce is the fastest (1x), increaseSync is 1.8x, and surprisingly, increaseWithAtomic is 112.5x slower. While specific numbers vary across machines, this clearly shows that when the computation itself is very fast compared to the overhead of ensuring a shared state remains synchronized, Using Synchronizations or Atomic Operations introduces massive contention penalty.
Conclusion
When addressing Race Conditions today, Avoiding shared state represents the most performant strategy in the vast majority of cases. Map-Reduce is an effective demonstration of this principle, with many other patterns available for complex requirements. Check out upcoming posts in the Kotlin Coroutines series for more concurrent programming challenges and patterns.
The sample code is updated in this repo. Thanks for reading!