Understanding the digital brain in robots.
In this blog, we’ll delve into how computers think and explore how understanding these foundational concepts can enhance your robotics development. From the basic unit of computing to multi-threading, we’ll break down key concepts such as threads, processes, shared memory, and strategies for improving the efficiency of your programs.
The Central Processing Unit (CPU) serves as the brain of the computer, responsible for interpreting code instructions and executing programs through basic arithmetic and logical operations. The smallest unit of processing within the CPU is known as a thread. When executing a thread, the CPU sequentially processes a set of functions, such as moving data between registers or performing operations on the data.
For instance, to execute an addition, the CPU first stores the numbers in its registers. The Arithmetic Logic Unit (ALU) then performs the operation, with the result saved in a new register. All of this occurs under the direction of the Control Unit (CU), which manages and coordinates the operations to ensure accurate execution.
The next unit of computing is the process. A process represents an instance of a program being executed and can contain one or more threads. Threads within the same process share memory resources, such as registers and variables, allowing them to exchange information quickly and efficiently. This shared memory access enables multithreaded processes to accelerate program execution by reducing the overhead of duplicating or transferring data.
In contrast, separate processes operate in isolated memory spaces, meaning they cannot directly share information. To communicate, different processes must rely on additional mechanisms, such as inter-process communication (IPC) protocols like pipes, shared files, or message queues. While multithreading offers a performance boost by leveraging shared memory, the additional steps required for communication between processes can introduce complexity and overhead. This trade-off makes it essential to consider the specific requirements of your program when deciding between multithreading or multiple processes.
Typically, a process is executed by a single core of a CPU, with each core capable of executing a sequential list of instructions. However, modern computers achieve the illusion of multitasking through multiprocessing, rapidly switching between processes to appear as though many tasks are running simultaneously. Additionally, multi-core CPUs enhance this capability by providing multiple cores that can execute different processes concurrently, offering true parallelism.
Threads within the same process have a significant advantage when it comes to data access, as they share the same memory registers. This allows for faster communication and data handling within the process. Conversely, processes must rely on shared-memory registers or other inter-process communication mechanisms to exchange information. For example, Process ‘A’ may write data to shared memory and notify Process ‘B’ when the data is ready. While this shared-memory exchange is slower than accessing registers, it remains an efficient means of communication.
The trade-off lies in isolation. Multiple processes are inherently more robust because they are isolated from each other. If one process encounters an error or becomes unresponsive, the others can continue functioning without disruption. For instance, when you see a “Not Responding” message in one program, the rest of your computer usually remains operational. This isolation is a direct benefit of running each program in its own process, ensuring that failures in one do not cascade to others.
Threads require synchronization methods to coordinate their workflows and avoid conflicts when accessing shared memory. One of the most basic synchronization primitives is the semaphore, which represents a pool of resources or work capacity available.
Below is a simple example of using a semaphore in Python.
import threading
import time
import random
# Semaphore initialized with a capacity of 3
semaphore = threading.Semaphore(3)
# Example function
def worker(thread_id):
print(f"Thread-{thread_id} waiting for access...")
semaphore.acquire() # Acquire access to the resource
print(f"Thread-{thread_id} has access!")
time.sleep(random.uniform(1, 3)) # Simulate some work
print(f"Thread-{thread_id} releasing access.")
semaphore.release() # Release access to the resource
# Create and start more than 3 threads
threads = []
for i in range(6):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
# Join threads
for t in threads:
t.join()
A mutex (or lock) is another synchronization mechanism, designed to prevent more than one thread or process from accessing a critical section of code simultaneously, especially when shared memory is involved. In the example below, a shared variable counter is accessed and modified by multiple threads at different moments. The mutex ensures that only one thread can update the variable at a time, avoiding race conditions.
import threading
# Shared resource
counter = 0
# Mutex lock
mutex = threading.Lock()
def increment(thread_id):
global counter
print(f"Thread-{thread_id} trying to lock the mutex.")
with mutex: # Acquires and releases lock automatically
print(f"Thread-{thread_id} has locked the mutex.")
current_value = counter
print(f"Thread-{thread_id} reads counter: {current_value}")
counter = current_value + 1
print(f"Thread-{thread_id} increments counter to: {counter}")
print(f"Thread-{thread_id} released the mutex.")
# Create and start multiple threads
threads = []
for i in range(6):
t = threading.Thread(target=increment, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
There are many other synchronization methods available, with numerous examples and resources online. We encourage you to explore these options to better understand their strengths, weaknesses, and the scenarios where each is most effective. Developing a solid understanding of these tools will help you choose the best approach for your specific use case.
Most robots rely on computers as their central processing units, acting as their brains. Leveraging multi-threaded processes and intra-process communication can significantly enhance a robot’s performance and reliability.
Consider the typical robotic workflow: sense, think, and act. If each step operates in a separate process, every time new data is generated, communication between processes introduces a small but cumulative delay to the overall processing time. However, by implementing multi-threaded processes where some or all steps share the same memory space, data transmission becomes significantly faster. Threads within the same process can access shared memory directly, eliminating the need for slower shared-memory exchanges between processes.
This approach allows robots to process sensory data, make decisions, and execute actions more efficiently, enabling faster responses and smoother operations. By optimizing multi-threading and intra-process communication, robotic systems can achieve improved real-time performance and robustness, especially in environments requiring rapid decision-making and precision.
For example, imagine a sensor generating massive amounts of data, such as a LIDAR. Sharing this data through shared memory can be time-consuming and inefficient.
Note: the numbers in the following images are examples, not real values of time.
Using the available memory in a single process can speed up the data communication between threads.
For slow-moving robots, this might not seem critical. However, for fast robots like drones, autonomous cars on highways, or dexterous humanoids, every microsecond matters to ensure accurate navigation and interaction with their environment.
This post serves as a brief introduction to processes and threads, touching on foundational concepts that are central to computation. Multi-threading, while powerful, is one of the most complex topics in programming, requiring a deep understanding of synchronization, resource sharing, and performance optimization.
In the next post, we’ll explore how to apply multi-threading in ROS 2 using Composable Nodes. These nodes are designed to run within a single process, leveraging the benefits of shared memory and efficient communication that we’ve discussed here. By understanding and implementing these concepts, you’ll be able to optimize your robotic systems for faster and more reliable performance. Stay tuned!