RCU as a solution

The root of the race condition that we are encountering is, as we know, the fact that the network object that we are working with is being shared between different threads, which are mutating and reading the data from the data structure simultaneously. Specifically, the second thread in our program was mutating the data (by calling the refresh_primary() method), while the first thread was reading from the same data.

Obviously, we can simply apply locking as the synchronization mechanism for this data structure. However, we know that the tasks of acquiring and releasing locks involve a slight cost that will become substantial as the data structure is widely used across a system. As popular websites and systems (namely, MongoDB) use this abstraction to design and structure their servers, a considerably high level of traffic will make the cost of using locks apparent, and cause the performance to decrease. Implementing a variation of an approximate data structure could help with this issue, but the complexity of the implementation might prove to be too difficult to follow through.

Thus, we arrive at the goal of using a mutex-free approach as our synchronization mechanism—in this case, read-copy-update (RCU). To protect the integrity of your data structure, RCU is, in essence, a synchronization mechanism that creates and maintains another version of the data structure when a thread or process requests read or write access to it. By isolating the interaction between the data structure and threads/processes within a separate copy, RCU ensures that no conflicting data can occur. As a thread or a process has mutated the information in the copy of the data structure that it is assigned to, that update can then be reported to the original data structure.

In short, when a shared data structure has threads or processes requesting access to it (the read process), it needs to return a copy of itself, instead of letting the threads/processes access its own data (the copy process); finally, if there are any changes in the copies of the data structure, they will need to be updated back to the shared data structure (the update process).

RCU is particularly useful for data structures that have to handle a single updater and multiple readers at the same time, which is the typical case of the server network that we discussed previously (multiple clients constantly accessing and requesting data, but only occasional, periodic attacks). But how would this apply to our current network data structure? Theoretically, the accessor method of our data structure (the get_primary_value() method), which is, again, the root of the race condition, needs to create a copy of the data structure before reading the data from a thread. This specification is implemented in the accessor method, in the Chapter16/concurrent_network.py file, as follows:

# Chapter16/concurrent_network.py

from copy import deepcopy
import time

class Network:
[...]

def get_primary_value(self):
copy_network = deepcopy(self)

primary_key = copy_network.primary_key
time.sleep(1) # creating a delay
return copy_network.data[primary_key]

Here, we are using the built-in deepcopy method from the copy module, which returns a separate copy of our network in a different memory location. Then, we only read the data from this copy of the network object, and not the original object itself. This process is illustrated in the following diagram:

RCU addressing the race condition

In the preceding diagram, we can see that no conflict will occur in terms of data, as the two threads now deal with different copies of the data structure. Let us see this implementation in action in the Chapter16/example6.py file, which contains the same instructions as the previous example5.py file (initializing a network object, calling two threads at the same time—one to access the primary data of the network, the other to refresh the same primary data), only now the program is using our new data structure from the concurrent_network.py file.

After running the script, your output should be the same as the following:

> python3 example6.py
Initial network: {
A: 1;
}

Full network: {
A: 1;
B: 1;
C: 1;
}

Current primary value: 1.
Final network: {
B: 1;
C: 1;
}

Finished.

As you can see, not only does the program obtain the correct value of the primary data in the first thread without evoking any errors, it also holds the correct network at the end of the program (without the previously deleted node, with the key A). The RCU method does, indeed, solve the problem of the race condition, without the use of any locking mechanisms.

One thing that you might have also noticed is that RCU could also be applied for our counter example in the previous section. It is true that both RCU and approximate counters are reasonable approaches to the counter problem, and the question of which one is the better solution for a specific concurrent problem can only be answered by empirical, hands-on analysis such as scalability analysis.

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

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