LocklessCounter and race conditions

First, let's simulate the problem encountered with a naive, lockless implementation of a counter class in a concurrent program. If you have already downloaded the code for this book from the GitHub page, go ahead and navigate to the Chapter16 folder.

Let us take a look at the Chapter16/example1.py file—specifically, the implementation of the LocklessCounter class:

# Chapter16/example1.py

import time

class LocklessCounter:
def __init__(self):
self.value = 0

def increment(self, x):
new_value = self.value + x
time.sleep(0.001) # creating a delay
self.value = new_value

def get_value(self):
return self.value

This is a simple counter that has an attribute called value, which contains the current value of the counter, assigned with 0 when the counter instance is first initialized. The increment() method of the class takes in an argument, x, and increases the current value of the calling LocklessCounter object by x. Notice that we are creating a small delay inside the increment() function, between the process of computing the new value of the counter and the process of assigning that new value to the counter object. The class also has a method called get_value(), which returns the current value of the calling counter.

It is quite obvious why this implementation of the LocklessCounter class can create a race condition in a concurrent program: while a thread is in the middle of incrementing a shared counter, another thread also might access the counter to execute the increment() method, and the change to the counter value made by the first thread might be overwritten by the one made by the second thread.

As a refresher, the following diagram shows how a race condition can occur in situations where multiple processes or threads access and mutate a shared resource at the same time:

Diagram of a race condition

To simulate this race condition, in our main program we are including a total of three threads, to increment a shared counter by 300 times:

# Chapter16/example1.py

from concurrent.futures import ThreadPoolExecutor

counter = LocklessCounter()
with ThreadPoolExecutor(max_workers=3) as executor:
executor.map(counter.increment, [1 for i in range(300)])

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

The concurrent.futures module offers us an easy and high-level way to schedule a task through a pool of threads. Specifically, after initializing a shared counter object, we declare the variable executor as a pool of three threads (use a context manager), and that executor calls the increment() method on the shared counter 300 times, each time incrementing the value of the counter by 1.

These tasks are to be executed among the three threads in the pool, using the map() method of the ThreadPoolExecutor class. At the end of the program, we simply print out the final value of the counter object. The following code shows my own output after running the script:

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

While it is possible to obtain a different value for the counter when executing the script on your own system, it is extremely unlikely that the final value of the counter will actually be 300, which is the correct value. Additionally, if you were to run the script over and over again, it would be possible to obtain different values for the counter, illustrating the non-deterministic nature of the program. Again, as some threads were overwriting the changes made by other threads, some increments got lost during the execution, resulting in the fact that the counter was only successfully incremented 101 times, in this case.

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

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