Mahdi.Kh
September 18, 2026
A look at multi-threaded programming and the race condition problem, with an example.
In many older operating systems, the process was the only formal unit of execution, and if a programmer wanted to do several things at once, they'd create several separate processes that talked to each other using inter-process communication mechanisms (IPC).
In this article, we briefly explain why and how multi-processing was used, why threads (Threads) came about, the differences and use cases of each approach, and a few short examples.
Process: An independent unit of execution with its own separate address space, file descriptors, and resources. The dominant model in early Unix systems and many others.
Thread: A lighter-weight unit inside a process that has a shared address space. Standards such as POSIX Threads (pthreads) and OS APIs later made the use of threads widespread.
In an era when threads either didn't exist or weren't stable and standardized, the practical way to achieve concurrency was multi-processing.
The program had a main process that, for every parallel task, spawned a child process (using fork() on Unix, CreateProcess on Windows, and so on).
To exchange data between them, IPC was used
This approach had good isolation, but it was heavier than threads: more expensive to create/destroy, more costly context switches, and harder data sharing. (One reason for this was the need to recreate memory variables for each process and the lack of shared memory.)
Performance: Creating and switching between threads is cheaper than doing so between processes.
Shared memory: All threads of a process have access to shared data, so exchanging data is simpler and cheaper.
Programming model: For many problems (for example, servers with a large number of connections), it's simpler to keep the logic in one shared address space and use a thread for each task.
Of course, shared memory means careful synchronization (locks, condition variables, atomics) is required, along with attention to race conditions, which we'll look at next with an example.
Suppose we want to sum the numbers from 1 to 1,000,000, but to speed up the operation, we want to do it in two threads at the same time. The correct answer we expect is 500000500000. We write the code for it in Golang as follows:
Running this code each time gives us a different answer, and none of them are correct. The reason is the concurrent writes to the variable a. That is, while the first function wants to save the new value of a, the second function is saving its new value at the same time, and as a result the values aren't saved sequentially and correctly. Let's go through this more carefully with an example. Suppose we have two actors, named Ali and Hamed, each responsible for incrementing a number written on a table (which, in the earlier code, is a CPU register). The sequence of events is as follows:
The number on the paper is 10.
Ali looks at the number: "It's 10; I'll turn it into 11," but hasn't written anything on the paper yet.
At the same moment, Hamed also arrives and sees the number: "It's 10; I'll turn it into 11 too."
Hamed writes before Ali does, and the paper changes to 11.
Then Ali comes back and, relying on what he saw earlier (10), writes 11.
Final result: the number on the paper stays 11. Whereas after both of them finished, we'd expect the value to reach 12. This is exactly the race condition phenomenon: the operations collide, and one of the increments is effectively lost. The read and write happen right at the line a = a + i. This line is called the critical section (Critical Section). So what's the solution?
To fix this problem in the previous example, we protect the critical section so it can't run concurrently. So our code changes as follows: