Thread Atomic Operations in Python

March 12, 2022 Python Threading

Operations like assignment and adding values to a list or a dict in Python are atomic.

In this tutorial you will discover thread atomic operations in Python.

Let's get started.

Atomic Operations

An atomic operation is one or a sequence of code instructions that are completed without interruption.

A program may be interrupted for one of many reasons. In concurrent programming, a program may be interrupted via a context switch.

You may recall that the operating system controls what threads execute and when. A context switch refers to the operating system pausing the execution of a thread and storing its state, while unpausing another thread and restoring its state.

A thread cannot be context switched in the middle of an atomic operation.

This means these operations are thread-safe as we can expect them to be completed once started.

Now that we know what an atomic operation is, let's look at some examples in Python.

Atomic Operations in Python

A number of operations in Python are atomic.

Under the covers, the Python interpreter that runs your program executes Python bytecodes in a virtual machine, called the Python Virtual Machine (PVM). These are a lower-level set of instructions and provide the basis for both context switching between threads and atomic operations.

Specifically, a Python program will be context switched at the level of Python bytecodes. A Python program will also be atomic at the level of Python bytecodes.

In general, Python offers to switch among threads only between bytecode instructions

-- What kinds of global value mutation are thread-safe?, Library and Extension FAQ

Nevertheless, a number of standard Python operations are atomic at both the Python code and bytecode level. This means that the operations are thread-safe under the reference Python interpreter (CPython) at the time of writing

The Python FAQ provides a useful list of these operations.

Let's review some of these atomic operations in Python.

Atomic Assignment

Assigning a value to a variable is atomic.

For example:

...
# assignment is atomic
x = 44
y = 33
x = y

Assigning a value to an object property is atomic.

For example:

...
# assigning a property is atomic
x.value = 33

Atomic Lists Operations

Many operations on lists are atomic.

Adding a value to a list is atomic.

For example:

...
# adding a value is atomic
a.append(3)

Adding one list to the end of another list is atomic.

For example:

...
# adding a list to a list is atomic
a.extend(b)

Retrieving a value from a list is atomic by de-referencing its index.

For example:

...
# getting a value from a list is atomic
value = a[2]

Removing a value from a list is atomic.

For example:

...
# removing a value from a list is atomic
value = a.pop()

Assigning a slice of the list is atomic.

For example:

...
# assigning a slice of the list is atomic
a[1:4] = b

Sorting a list is atomic.

For example:

...
# sorting the list is atomic
a.sort()

Atomic Dict Operations

Some operations on a dictionary are atomic.

Assigning a value to a key on the dict is atomic.

For example:

...
# assigning a value to a key is atomic
d = 22

Combining one dict into another is atomic.

For example:

...
# adding a dict to a dict is a atomic
a.update(b)

Retrieving the keys from a dict is atomic.

For example:

...
# getting keys is atomic
keys = x.keys()

Now that we are familiar with atomic operations in Python, let's look at some operations that are not atomic.

Non-Atomic Operations in Python

Most operations are not atomic in Python.

This means that these operations are not thread-safe.

In this section we will discuss a few non-atomic operations that when used in concurrent programs can lead to a concurrent failure condition or bug called a race condition.

Adding and Subtracting a Variable

Adding or subtracting a value from a variable, such as an integer variable, is not atomic.

For example:

...
# adding and subtracting is not atomic
a = a + 1

The reason for this is at least three operations are involved, they are:

  1. Read the value of the variable.
  2. Calculate the new value for the variable
  3. Assign the calculated value of the variable.

Access and Assign

Combining the access and assignment of a value in a list or dict and assignment is not atomic.

For example:

...
# access and assign is not atomic
a[0] = b[0]

Now that we know some examples of operations that are not atomic, let's consider some recommendations regarding atomic operations.

Recommendations Regarding Atomic Operations

Although some operations are atomic in Python, we should never rely on an operation being atomic.

There are a number of good reasons for this, such as:

As such, you should not rely on the built-in atomic operations listed above. In most cases, you should act as they are not available.

A similar stance is recommended in the Google Python style guide.

For example:

Do not rely on the atomicity of built-in types.

While Python’s built-in data types such as dictionaries appear to have atomic operations, there are corner cases where they aren’t atomic (e.g. if __hash__ or __eq__ are implemented as Python methods) and their atomicity should not be relied upon. Neither should you rely on atomic variable assignment (since this in turn depends on dictionaries).

Use the Queue module’s Queue data type as the preferred way to communicate data between threads. Otherwise, use the threading module and its locking primitives. Prefer condition variables and threading.Condition instead of using lower-level locks.

-- Section 2.18 Threading, Google Python Style Guide

This is excellent advice.

So, if we should not rely on built-in atomic operations in Python, what should we do instead?

Next, let's look at the alternative.

Make Operations Atomic

When atomic operations are required, use a lock.

In concurrency programming, it is common to have critical sections of code that may be executed by multiple threads simultaneously which must be protected.

These sections can be protected using locks such as the mutual exclusion lock (mutex) provided in the threading.Lock class.

If you are new to the threading.Lock class, you can learn more here:

This class allows you to define arbitrary blocks of code, from one line, to entire functions of code that can be treated as an atomic block.

For example, the context manager for the threading.Lock can be used:

...
# protect a critical section
with lock:
	# ....

Using the lock to protect a block of code does not prevent the thread from being context switched in the middle of an instruction or between instructions in the block.

Instead, it prevents other threads from executing the same block while a thread holds the lock.

The effect is a simulated atomic operation or sequence of instructions in your program that can be used to protect data, variables, and state shared between threads.

Takeaways

You now know about atomic operations in Python.



If you enjoyed this tutorial, you will love my book: Python Threading Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.