DEV Community

Turbo Ninh
Turbo Ninh

Posted on

Understanding Race Conditions with Typescript example

Race condition is a scenario in computer programming where two or more threads (or processes) access a shared resource simultaneously and the outcome is dependent on the timing of the access. The final result can be unpredictable and unexpected because the order in which the threads access the resource can change each time the program is run.

For example, consider a scenario where two people, Alice and Bob, are trying to withdraw money from a shared bank account at the same time. If Alice checks the balance and sees that there is enough money to withdraw, she takes out $100. At the same time, Bob also checks the balance and sees that there is enough money to withdraw, so he takes out $100 as well. The final result is that the account balance has decreased by $200 instead of $100, which is incorrect. This is an example of a race condition because the outcome of the action depends on the timing of the access to the shared resource (the bank account).

Race conditions can occur in a variety of situations, including multithreaded applications, distributed systems, and concurrent programming. To avoid race conditions, it is important to understand the conditions that cause them and to implement proper synchronization mechanisms to control access to shared resources.

Let's see a real-world example of a race condition in a Typescript application. In this example, we will create a simple program that increments a counter and outputs the final result.

let counter = 0;

async function incrementCounter() {
  counter++;
  console.log(counter);
}

async function run() {
  for (let i = 0; i < 10; i++) {
    incrementCounter();
  }
}

run();
Enter fullscreen mode Exit fullscreen mode

When we run this code, we expect the counter to be incremented 10 times and the final result to be 10. However, because the incrementCounter function is not synchronized, the final result can be unexpected and may not equal 10. This is because multiple calls to the incrementCounter function can occur simultaneously, resulting in multiple threads trying to increment the counter at the same time. To avoid this race condition, we can use a mechanism such as a lock to synchronize access to the counter.

let counter = 0;
let lock = false;

async function incrementCounter() {
  while (lock) {
    // Wait until the lock is released
  }

  lock = true;
  counter++;
  console.log(counter);
  lock = false;
}

async function run() {
  for (let i = 0; i < 10; i++) {
    incrementCounter();
  }
}

run();
Enter fullscreen mode Exit fullscreen mode

In this updated version of the code, the lock variable acts as a semaphore to ensure that only one thread can access the counter at a time. Before incrementing the counter, the function checks the value of the lock. If it is set to true, the function waits until it is set to false. Once the lock is acquired, the counter is incremented, the result is logged, and the lock is released. This ensures that the counter is incremented in a predictable and controlled manner, avoiding the race condition.

In conclusion, race conditions can have significant impacts on the correctness of a program and can lead to unpredictable and unexpected results. To avoid race conditions, it is important to understand the conditions that cause them and to implement proper synchronization mechanisms to control access to shared resources. In the example we saw, using a lock was one way to synchronize access to the shared resource and avoid the race condition.

Top comments (0)