Simulation in Python

Let's analyze the difference between an atomic operation and a nonatomic one in an actual Python concurrent program. If you already have the code for this book downloaded from the GitHub page, go ahead and navigate to the Chapter17 folder. For this example, we are considering the Chapter17/example1.py file:

# Chapter17/example1.py

import sys; sys.setswitchinterval(.000001)
import threading

def foo():
global n
n += 1

n = 0

threads = []
for i in range(1000):
thread = threading.Thread(target=foo)
threads.append(thread)

for thread in threads:
thread.start()

for thread in threads:
thread.join()

print(f'Final value: {n}.')

print('Finished.')

First of all, we are resetting the thread-switching frequency of the Python interpreter to 0.000001 seconds—this is to have the thread switching event take place more often than usual and thus amplify any race condition that might be in our program.

The gist of the program is to increment a simple global counter (n) with 1,000 separate threads, each incrementing the counter once via the foo() function. Since the counter was originally initialized as 0, if the program executed correctly, we would have that counter holding the value of 1,000 at the end of the program. However, we know that the increment operator that we are using in the foo() function (+=) is not an atomic operation, which means it can be interrupted by a thread-switching event when applied on a global variable.

After running the script multiple times, we can observe that there is, in fact, a race condition existing in our code. This is illustrated by incorrect values of the counter that are less than 1,000. For example, the following is an output I obtained:

> python3 example1.py
Final value: 998.
Finished.

This is consistent with what we have previously discussed, that is, since the += operator is not atomic, it would need other synchronization mechanisms to ensure the integrity of the data that it interacts with from multiple threads concurrently. Let's now simulate the same experiment with an operation that we know is atomic, specifically appending a predefined object to a list.

In the Chapter17/example2.py file, we have the following code:

# Chapter17/example2.py

import sys; sys.setswitchinterval(.000001)
import threading

def foo():
global my_list
my_list.append(1)

my_list = []

threads = []
for i in range(1000):
thread = threading.Thread(target=foo)
threads.append(thread)

for thread in threads:
thread.start()

for thread in threads:
thread.join()

print(f'Final list length: {len(my_list)}.')

print('Finished.')

Instead of a global counter, we now have a global list that was originally empty. The new foo() function now takes this global list and appends the integer 1 to it. In the rest of the program, we are still creating and running 1,000 separate threads, each of which calls the foo() function once. At the end of the program, we will print out the length of the global list to see if the list has been successfully mutated 1,000 times. Specifically, if the length of the list is less than 1,000, we will know that there is a race condition in our code, similar to what we saw in the previous example.

As the list.append() method is an atomic operation, however, it is guaranteed that there is no race condition when the threads call the foo() function and interact with the global list. This is illustrated by the length of the list at the end of the program. No matter how many times we run the program, the list will always have a length of 1,000:

> python3 example2.py
Final list length: 1000.
Finished.

Even though some operations in Python are innately atomic, it can be quite difficult to tell whether a given operation is atomic on its own or not. Since the application of nonatomic operations on shared data can lead to race conditions and thus erroneous results, it is always recommended that programmers utilize synchronization mechanisms to ensure the integrity of the shared data within a concurrent program.

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

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