Simulating race conditions in Python

Before we discuss a solution that we can implement to solve the problem of race conditions, let's try to simulate the problem in Python. If you have already downloaded the code for this book from the GitHub page, go ahead and navigate to the Chapter14 folder. Let's take a look at the Chapter14/example1.py file—specifically, the update() function, as follows:

# Chapter14/example1.py

import random
import time

def update():
global counter

current_counter = counter # reading in shared resource
time.sleep(random.randint(0, 1)) # simulating heavy calculations
counter = current_counter + 1 # updating shared resource

The goal of the preceding update() function is to increment a global variable called counter, and it is to be called by a separate thread in our script. Inside the function, we are interacting with a shared resource—in this case, counter. We then assign the value of counter to another local variable, called current_counter (this is to simulate the process of reading data from more complex data structures for the shared resources).

Next, we will pause the execution of the function by using the time.sleep() method. The length of the period during which the program will pause is pseudo-randomly chosen between 0 and 1, generated by the function call, random.randint(0, 1), so the program will either pause for one second or not at all. Finally, we assign the newly computed value of current_counter (which is its one-increment) to the original shared resource (the counter variable).

Now, we can move on to our main program:

# Chapter14/example1.py

import threading

counter = 0

threads = [threading.Thread(target=update) for i in range(20)]

for thread in threads:
thread.start()
for thread in threads:
thread.join()

print(f'Final counter: {counter}.')
print('Finished.')

Here, we are initializing the counter global variable with a set of threading.Thread objects, in order to execute the update() function concurrently; we are initializing twenty thread objects, to increment our shared counter twenty times. After starting and joining all of the threads that we have, we can finally print out the end value of our shared counter variable.

Theoretically, a well-designed concurrent program will successfully increment the share counter twenty times in total, and, since its original value is 0, the end value of the counter should be 20 at the end of the program. However, as you run this script, the counter variable that you obtain will most likely not hold an end value of 20. The following is my own output, obtained from running the script:

> python3 example1.py
Final counter: 9.
Finished.

This output indicates that the counter was only successfully incremented nine times. This is a direct result of a race condition that our concurrent program has. This race condition occurs when a specific thread spends time reading in and processing the data from the shared resource (specifically, for one second, using the time.sleep() method), and another thread reads in the current value of the counter variable, which, at this point, has not been updated by the first thread, since it has not completed its execution.

Interestingly, if a thread does not spend anytime processing the data (in other words, when 0 is chosen by the pseudo-random random.randint() method), the value of the shared resource can potentially be updated just in time for the next thread to read and process it. This phenomenon is illustrated by the fact that the end value of the counter varies within different runs of the program. For example, the following is the output that I obtained after running the script three times. The output from the first run is as follows:

> python3 example1.py
Final counter: 9.
Finished.

The output from the second run is as follows:

> python3 example1.py
Final counter: 12.
Finished.

The output from the third run is as follows:

> python3 example1.py
Final counter: 5.
Finished.

Again, the final value of the counter is dependent on the number of threads that spend one second pausing and the number of threads not pausing at all. Since these two numbers are, in turn, dependent on the random.randint() method, the final value of the counter changes between different runs of the program. We will still have a race condition in our program, except for when we can ensure that the final value of the counter is always 20 (the counter being successfully incremented twenty times, in total).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.142.85