Last Updated on September 12, 2022
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.
Run loops using all CPUs, download your FREE book to learn how.
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:
1 2 3 4 5 |
... # assignment is atomic x = 44 y = 33 x = y |
Assigning a value to an object property is atomic.
For example:
1 2 3 |
... # 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:
1 2 3 |
... # adding a value is atomic a.append(3) |
Adding one list to the end of another list is atomic.
For example:
1 2 3 |
... # 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:
1 2 3 |
... # getting a value from a list is atomic value = a[2] |
Removing a value from a list is atomic.
For example:
1 2 3 |
... # removing a value from a list is atomic value = a.pop() |
Assigning a slice of the list is atomic.
For example:
1 2 3 |
... # assigning a slice of the list is atomic a[1:4] = b |
Sorting a list is atomic.
For example:
1 2 3 |
... # 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:
1 2 3 |
... # assigning a value to a key is atomic d[x] = 22 |
Combining one dict into another is atomic.
For example:
1 2 3 |
... # adding a dict to a dict is a atomic a.update(b) |
Retrieving the keys from a dict is atomic.
For example:
1 2 3 |
... # 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:
1 2 3 |
... # adding and subtracting is not atomic a = a + 1 |
The reason for this is at least three operations are involved, they are:
- Read the value of the variable.
- Calculate the new value for the variable
- 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:
1 2 3 |
... # 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.
Free Python Threading Course
Download your FREE threading PDF cheat sheet and get BONUS access to my free 7-day crash course on the threading API.
Discover how to use the Python threading module including how to create and start new threads and how to use a mutex locks and semaphores
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:
- The operations may not be atomic if the code is executed by a different Python interpreter.
- The behavior of the reference Python interpreter may change in the future.
- Other programmers who have to read your code may not be as intimately familiar with Python atomic operations.
- You may introduce more complex race conditions, e.g. operations are atomic but multiple such operations in a batch are not protected.
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.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
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:
1 2 3 4 |
... # 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.
Further Reading
This section provides additional resources that you may find helpful.
Python Threading Books
- Python Threading Jump-Start, Jason Brownlee (my book!)
- Threading API Interview Questions
- Threading Module API Cheat Sheet
I also recommend specific chapters in the following books:
- Python Cookbook, David Beazley and Brian Jones, 2013.
- See: Chapter 12: Concurrency
- Effective Python, Brett Slatkin, 2019.
- See: Chapter 7: Concurrency and Parallelism
- Python in a Nutshell, Alex Martelli, et al., 2017.
- See: Chapter: 14: Threads and Processes
Guides
- Python Threading: The Complete Guide
- Python ThreadPoolExecutor: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Takeaways
You now know about atomic operations in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Harley-Davidson on Unsplash
Do you have any questions?