Race Conditions

5 min read

Image
What happens when everybody wants the same in the same time.

When you read this article, you are using some kind of computer, whether it’s a laptop, PC, smartphone, or tablet. This device runs an operating system, such as Windows, Linux, macOS, or Android. You can do many things on it simultaneously, like listening to music, programming, writing a novel, or watching your favorite movies. But does everything truly happen at the same time? If so, how does it work? What’s the secret behind it? Well, there is no magic - only smart engineering.

It’s Like a Movie

You know how movies work: they are a series of still images shown in rapid succession. At around 24 frames per second, the human eye perceives them as a continuous motion. The same concept applies to your computer. Every running program gets a short slice of time to use system resources (processor, memory, display, storage, etc.). Once its turn is over, it pauses while another program gets access.

Programs do not execute at the same time - they take turns so rapidly that humans perceive it as simultaneity. This start-stop scheduling happens so quickly that it’s imperceptible to us, just like the frames in a movie.

Sharing Resources

We now know that programs are scheduled for access to system resources. But let’s go deeper, down to a single program’s level. From the operating system’s perspective, a running program is called a process. A key characteristic of a process is that it does not share memory with other processes. The memory used by Process A is inaccessible to Process B.

Imagine if a web browser could read memory from an image editor. A malicious website could scan your computer’s memory and steal images from your editing software. Fortunately, operating systems enforce memory isolation to prevent this.

However, within a single process, things work differently. A process can spawn threads, and these threads do share memory. Since they belong to the same process, they operate in the same context and can freely access shared data. But this can introduce serious problems. Each thread runs independently, and when multiple threads modify the same data, things can go wrong.

The Thread Experiment

Let’s do an experiment to illustrate the issue.

  1. Create a text file called memory.txt and write: “My name is Earl.” Save the file and keep the editor open.
  2. Open memory.txt in a second text editor instance.
  3. In the second editor, add a new line: “Nice to meet you, Earl.” and save it.
  4. Now go back to the first editor and save the file again.
  5. Close both editors and reopen memory.txt. What do you see?

You might expect both lines to be present, but one may be missing. What happened?

Since each editor instance acted as an independent process, they treated the file as a shared memory resource. Thread 1 (T1) read the file, added a line, and saved it. Thread 2 (T2) did the same but was unaware of T1’s changes. When T2 saved its version, it overwrote T1’s update. This is an example of data racing.

Data Racing

The experiment shows how dangerous it can be when multiple threads or processes write to the same data source. Every thread, at its lowest level, follows these three steps:

  1. Read data
  2. Process data
  3. Write data

The problem arises because there’s always a time gap between reading and writing. If another thread modifies the data in that gap, the first thread is working with outdated information. This becomes especially problematic under high system load. You should assume that data is already obsolete the moment you retrieve it.

Understanding Race Conditions

You may have heard the term Race Condition used interchangeably with Data Race, but they are slightly different. While data races involve uncontrolled simultaneous access to shared data, race conditions refer to unpredictable behavior due to timing issues in concurrent execution.

Imagine you need to save a 10-second video clip from your webcam to a file. Your program is notified every time a frame is captured, and each frame is handled by a separate thread. The problem? Frames must be saved in the correct order. If they aren’t, the video becomes a jumbled mess. This is a Race Condition because the order of execution affects the final result.

Additionally, file saving happens asynchronously in separate threads. The last thread is responsible for merging all frames into a video file. However, if a thread lacks proper synchronization, it may never know when all frames have been saved, leading to an incomplete video or a program freeze.

Fixing the Clip Recorder

The simplest way to solve the first issue is proper file naming. The program should timestamp each frame and sort them before assembling the video.

The second issue is trickier. The program must track the number of frames received and processed. A global atomic counter can help. Each thread updates the counter, and when it reaches the expected total, the final processing step can safely begin.

Avoiding Race Conditions

Finding and fixing race conditions is difficult. Due to their asynchronous and unpredictable nature, they may only appear under certain conditions and at unexpected times. Fortunately, many programming languages, databases, web servers, and libraries offer built-in mechanisms to prevent them, such as:

Race conditions are tricky but manageable with the right tools and strategies. Proper planning, understanding concurrency, and using available safeguards can keep your programs stable and reliable.