A race condition is a situation in which a system's effects are dependent on the relative timing of when multiple processes get run. They're often created by accident in multi-threaded applications when two or more threads attempt to access and/or modify a shared variable around the same time, i.e. the threads race to access/modify the value of the same variable. Because they frequently create logical errors and are difficult to reproduce, race conditions are generally undesirable.

Tolerable race conditions

The classic example of a race condition is demonstrated with a bank account. Let's imagine that, for every bank account, there exists a withdraw function that depletes a certain amount of money from the account if there are enough funds for it. If not, an error is raised and no funds are withdrawn:

class NotEnoughFundsError(Exception):
    pass


class BankAccount:
    def __init__(self, initial_amount):
	    self.amount = initial_amount

    def withdraw(self, amount):
        if self.amount >= amount:
            self.amount = self.amount - amount
        else:
            raise NotEnoughFundsError

If a customer withdraws $100 from a bank account they have with a starting amount of $500, the withdraw occurs successfully:

account = BankAccount(500)
account.withdraw(100)
print(account.amount)  # 400

If this is a joint account and a second customer later withdraws $450 from that same bank account, an error is raised. The request is denied:

account.withdraw(450)
Traceback (most recent call last):
  File ".../example.py", line 21, in <module>
    account.withdraw(450)
  File ".../example.py", line 13, in withdraw
    raise NotEnoughFundsError
__main__.NotEnoughFundsError

This makes sense. There aren't enough funds in the account anymore.

But what if both customers had made their withdrawal requests at the same time? We know that one of the customers would have had their request denied due to low funds, but it may not necessarily be the customer we expect. What matters is the order in which the computer system processes the requests. So if the second customer's request is processed first, they withdraw money successfully, and the first customer's request gets denied.

Said another way, the relative timing for processing each of the two customer's requests affects the overall outcome of the system. This is a race condition. And for the most part, the outcome is tolerable.

Buggy race conditions

Most race conditions are error-prone. In the above example, customers were unable to withdraw more money from the bank account than the funds available, because the withdraw request was processed as an atomic unit, i.e. the entire function was completed from start to finish without breaks to complete other tasks. This is not always the case.

What happens if the withdraw request is no longer atomic? When two customers both make simultaneous requests to withdraw money, individual lines of code become interwoven, so they may run in this order:

account = BankAccount(500)

if self.amount >= amount:              # thread 1: 500 >= 100 --> True
    self.amount = self.amount - amount # thread 1: self.amount = 500 - 100 = 400
if self.amount >= amount:              # thread 2: 400 >= 450 --> False
    raise NotEnoughFundsError          # thread 2: error raised

Or they may run in this order:

account = BankAccount(500)

if self.amount >= amount:              # thread 1: 500 >= 100 --> True
if self.amount >= amount:              # thread 2: 500 >= 450 --> True
    self.amount = self.amount - amount # thread 1: self.amount = 500 - 100 = 400
    self.amount = self.amount - amount # thread 2: self.amount = 400 - 450 = -50

Whoa! In that second ordering, the bank account allowed both customers to withdraw the requested amounts and ended up with -$50 in the end. No error was raised even though funds were insufficient! This is an undesirable race condition.

To prevent such race conditions, developers use tools like semaphores to limit access to shared variables (like the bank account amount) and enforce atomicity for functions. These techniques allow applications to be thread-safe so that multi-thread applications can run without worrying about race conditions.