Threads in ROS and Python
Overview
This is a practical guide to threads in ROS, focused on python. There are two goals:
- Suggest basic patterns to generally make your code safe in the face of the multiple threads that ROS implicitly creates in python
- Explore threads in python and ROS through experimentation
Resources for learning more about threads in python are at the end of this page.
Thread Basics
What is a process?
- A process is an abstraction used by the Linux Kernel (and other operating systems) to segregate sequences of machine instructions into their own memory address space and schedule their execution time.
- The operating system constantly switches the code that the CPU is executing between the machine code in various processes, to provide the illusion of simultaneous execution. On multi-core CPU's a few processes can run simultaneously. However, there are usually more processes than available CPU cores.
- Each process thinks it has access to a full memory-address space: in reality the kernel is hiding the true physical addresses and managing access to these physical addresses. The addresses used by processes are referred to as virtual addresses
- Because each process has its own view of memory, sharing data between processes requires the intervention of the operating system.
What is a thread?
- A thread is subordinate unit of execution within a process
- Each process can have multiple threads each of which executes its respective machine code instructions simultaneously (as scheduled by the kernel)
- Every process has a "main" thread, which is the only code that executes when you or the functions you call do not create additional threads
- A given process's threads all share the same memory address space as the parent process
- Each thread has its own stack (for calling functions and storing local variables)
- The heap and global variables are shared by threads
- One thread can directly affect the memory used by another thread, without operating system intervention (in contrast to a process).
- If two threads access the same memory and are not coordinated properly, a bug called a race condition can occur.
- A race condition can result in data corruption
- It can also result in a deadlock, where no thread can continue executing
- The exact interleaving of the instructions across multiple threads is non-deterministic because it depends on how the kernel
schedules execution, which in turn depends on what else is happening on the computer
- Non-deterministic bugs in multi-threaded code may be from a race condition
- Be cautious and have a plan before introducing extra threads into your program
- - As the operating system switches between threads, it is possible for instructions to be interrupted before completing:
- Generally, a single python statement maps to multiple machine instructions and thus a statement can be interrupted in the middle of executing
- Consider
i = i + 1
. The thread can be interrupted in the middle of reading the value ofi
, adding1
to it, or storing the result ini
- In the meantime, another thread can modify
i
, leading to incorrect results.
- Atomic operations finish executing without interruption. Atomic operations guarantee that a complete result will be computed and seen by all threads.
- Synchronization primitives such as a mutex or a semaphore can be used to coordinate execution between threads
Threads in Python
- The most common python interpreter, CPython, implements a Global Interpreter Lock (GIL)
- The GIL simplifies the creation of the interpreter while preserving good single-threaded performance
- It also limits multi-threaded performance in python
- The GIL prevents multiple python instructions from executing simultaneously on a multi-core system.
- Thus, multi-threading with CPython is like multi-threading on a single-core CPU: the python code is not executed simultaneously
- Operations on each thread are interleaved (that is, the python interpreter executes some commands on one thread, then switches to another).
- The order of this interleaving is generally non-deterministic
- The GIL does not prevent all race-conditions
- You still need to synchronize threads that read/write to the same shared variables.
- Bugs that occur due to specific orderings of operations on multiple threads can still occur
- Non-atomic python statements can be interrupted before completion
- The GIL only applies to python bytecode instructions
- Python code can call C code, and that C code can bypass the GIL
- Python waits for a system function (e.g., to reading from a file), it is not executing bytecode. Thus, another thread can run while the other thread waits for the system.
- Thus, multiple-threads can improve performance of input/output bound python programs but not
computationally bound ones.
- CPU bound means performance is limited by the available CPU resources
- I/O bound means performance is limited by input/output operations (such as reading a file)
- Atomic Operations: The following operations are guaranteed to complete once started, prior to another thread being run
- Reading or writing a single variable of a basic type (int, float, string)
- Assigning an object to a variable (e.g., x = y)
- Reading an item from a list
- Modifying an item in a list
- Getting an item from a dictionary
- A complete list of python atomic operations
If you perform a non-atomic operation, the thread you are running on can be interrupted in the middle. Assume we have two threads, both performing
i = i + 1
, a non-atomic operation. Thus what is actually executed gets broken into several interruptable stepsi = 0 # Two threads are started Thread 1 | Thread 2 i = i + 1 | i = i + 1
Operations can happen in multiple ways leading to different results: for example
Thread 1, reads i, it is 0 Thread 2, reads i, it is 0 Thread 1 adds 1 to what it read, yielding 1 Thread 1 stores 1 in i Thread 1 reads i it is 1 Thread 1 adds 1 to what it read, yielding 2 Thread 2 adds 1 to what it read, yielding 1 Thread 1 stores 2 in i Thread 2 stores 1 in i
Threads in rospy
The threading model for rospy is poorly documented by ROS The information here comes from my own explorations and discussion here https://answers.ros.org/question/9543/rospy-threading-model/ (the whole series of answers).
Timers
rospy.Timer(Rate(hz), callback)
lets you execute a callback at a given frequency.- Each timer runs in its own thread as soon as you create the timer.
- While a timer callback is executing, it blocks that timer callback from running again.
- For example, if you have a timer with period 1 second, but your code takes 3 seconds to execute, The timer will effectively only trigger once every 3 seconds
- See Time for more details about time and timers in ROS
Subscribers
- In rospy, it seems that each topic you subscribe to gets one thread per publisher.
- This means that a given subscriber callback can execute in different threads.
- If you have multiple subscribers on the same topic in the same node, they execute on the same thread.
Service Handlers
- Service Handlers execute in their own threads
What this threading model means for you
- Be aware that the callbacks you write may operate in a separate threads
- These threads are different from the main thread of your program
- Synchronize between callbacks in the following order of complexity
- Only use local variables
- Use atomic operations:
A very common use case is that you use a service to signal a change in a timer that is publishing data.
If you set variables using atomic operations, which in python includes simple assignments, then you
should not run into issues or require explicit synchronization methods.
- Use a Queue, especially with a producer/consumer pattern. Queues are thread-safe in python. One thread puts data onto the queue, the other thread reads it from the queue.
- If you must, resort to the thread locking mechanisms provided by python
- If you need tasks to happen concurrently, put them in different nodes.
roscpp and threads
- The thread model for ROS is different in python and in C++
- In roscpp, there is a single thread unless you explicitly create multiple threads
- In C++ there is no GIL, thus threads can execute simultaneously on multi-core machines
- In C++ the order in which expressions are executed is not necessarily the order in which they were coded; therefore, you must be very careful when synchronizing threads
- See Callbacks and Spinning for more details
Threads Example
- Here is an interactive example for exploring threads in python Threads