UP | HOME

Threads in ROS and Python

Overview

This is a practical guide to threads in ROS, focused on python. There are two goals:

  1. Suggest basic patterns to generally make your code safe in the face of the multiple threads that ROS implicitly creates in python
  2. 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?

  1. 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.
  2. 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.
  3. 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
  4. Because each process has its own view of memory, sharing data between processes requires the intervention of the operating system.

What is a thread?

  1. A thread is subordinate unit of execution within a process
  2. 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
  3. 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).
  4. 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
  5. 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
  6. - 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 of i, adding 1 to it, or storing the result in i
    • In the meantime, another thread can modify i, leading to incorrect results.
  7. Atomic operations finish executing without interruption. Atomic operations guarantee that a complete result will be computed and seen by all threads.
  8. Synchronization primitives such as a mutex or a semaphore can be used to coordinate execution between threads

Threads in Python

  1. 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
  2. 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
  3. 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
  4. 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)
  5. 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
  6. 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 steps

    i = 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

  1. Be aware that the callbacks you write may operate in a separate threads
  2. These threads are different from the main thread of your program
  3. Synchronize between callbacks in the following order of complexity
    1. Only use local variables
    2. 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.
      1. 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.
      2. If you must, resort to the thread locking mechanisms provided by python
  4. If you need tasks to happen concurrently, put them in different nodes.

roscpp and threads

  1. The thread model for ROS is different in python and in C++
  2. In roscpp, there is a single thread unless you explicitly create multiple threads
  3. In C++ there is no GIL, thus threads can execute simultaneously on multi-core machines
  4. 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
  5. See Callbacks and Spinning for more details

Threads Example

  1. Here is an interactive example for exploring threads in python Threads

Python Resources

Author: Matthew Elwin