Race condition is a significant problem in concurrent programming. The condition occurs when one thread tries to modify a shared resource at the same time that another thread is modifying that resource – this leads to garbled output, which is why threads need to be synchronized.
The threading
module of Python includes locks as a synchronization tool. A lock has two states:
A lock can be locked using the acquire()
method. Once a thread has acquired the lock, all subsequent attempts to acquire the lock are blocked until it is released. The lock can be released using the release()
method.
Calling the
release()
method on a lock, in an unlocked state, results in an error.
The following code shows how locks can be used in Python with a simple example:
Suppose that there are 100 dollars in a bank account. Every month, 10 dollars are deposited as profit, and 10 dollars are deducted to pay a bill. thread1
deposits the profit and thread2
pays for the bill. In some months, the profit is deposited after the bill is paid. However, this should not affect the final amount in the account.
The final answer might be incorrect due to the race condition. There may be cases where a thread is unable to write the updated value to the shared variable deposit before the context switch, and the other thread reads a non-updated value; thus, leading to an unpredictable result.
You may have to run the code several times to re-create the error.
# Importing the threading moduleimport threadingdeposit = 100# Function to add profit to the depositdef add_profit():global depositfor i in range(100000):deposit = deposit + 10# Function to deduct money from the depositdef pay_bill():global depositfor i in range(100000):deposit = deposit - 10# Creating threadsthread1 = threading.Thread(target = add_profit, args = ())thread2 = threading.Thread(target = pay_bill, args = ())# Starting the threadsthread1.start()thread2.start()# Waiting for both the threads to finish executingthread1.join()thread2.join()# Displaying the final value of the depositprint(deposit)
The code between the acquire()
and release()
methods are executed atomically so that there is no chance that a thread will read a non-updated version after another thread has already made a change.
# Importing the threading moduleimport threading# Declraing a locklock = threading.Lock()deposit = 100# Function to add profit to the depositdef add_profit():global depositfor i in range(100000):lock.acquire()deposit = deposit + 10lock.release()# Function to deduct money from the depositdef pay_bill():global depositfor i in range(100000):lock.acquire()deposit = deposit - 10lock.release()# Creating threadsthread1 = threading.Thread(target = add_profit, args = ())thread2 = threading.Thread(target = pay_bill, args = ())# Starting the threadsthread1.start()thread2.start()# Waiting for both the threads to finish executingthread1.join()thread2.join()# Displaying the final value of the depositprint(deposit)