Skip to content

This PR adds a new class to manage threads - #11

Open
juanjqo wants to merge 13 commits into
SmartArmStack:jazzyfrom
juanjqo:jazzy_loopancho
Open

This PR adds a new class to manage threads #11
juanjqo wants to merge 13 commits into
SmartArmStack:jazzyfrom
juanjqo:jazzy_loopancho

Conversation

@juanjqo

@juanjqo juanjqo commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Hi @mmmarinho,

This PR adds the class sas::thread_manager (codename: loopancho) to manage threads. The main motivation is the standardisation of the SAS drivers (at least in the Adorno-lab::RAICo projects), which currently implement custom threads. This class is experimental, since I haven't tested it yet with real platforms. It is the first version and we can improve it.

Current features:

  • Based on modern C++ and the standard library (no Boost =) )
  • Support for priority levels (only for GNU/Linux)
  • The class relies on sas::Clock for time computations.
  • Thread-safety

Please let me know what you think and whether the design aligns with what you have in mind.

Minimal example

#include <sas_core/sas_thread_manager.hpp>
#include <iostream>
#include <thread>

int main() {
    int counter = 0;

    // Create and start thread
    sas::thread_manager tm("test", 0.1, [&counter]() {
        std::cout << ++counter << "\n";
    });

    tm.start();

    // Run for 1 second
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // Clean up
    tm.stop();
    return 0;
}

CMake

cmake_minimum_required(VERSION 3.16)

project(minimal_example LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)
FetchContent_Declare(
    sas_core
    GIT_REPOSITORY https://github.com/juanjqo/sas_core.git
    GIT_TAG        jazzy_loopancho
)
set(ROS2_BUILD OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(sas_core)
add_executable(minimal_example main.cpp)
target_link_libraries(minimal_example PRIVATE sas_core_pure)

output

./minimal_example
**************************************************************************
sas::Clock (c) Murilo M. Marinho (murilomarinho.info) 2016-2026 LGPLv3
**************************************************************************
1
2
3
4
5
6
7
8
9
10
11
17:05:20: The command "/home/juanjqo/Documents/sas_core_test_examples/thread_manager/minimal_example/build/Desktop_Qt_6_10_1-Debug/minimal_example" finished successfully.

Kind regards,

Juancho

@mmmarinho mmmarinho left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the submission. Some suggestions!

Comment thread include/sas_core/sas_thread_manager.hpp Outdated
Comment thread include/sas_core/sas_thread_manager.hpp
void thread_manager::stop()
{
if (!running_.exchange(false)) {
return; // Not running

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you don't need to return early but I'm happy to be corrected. Isn't it ok to always check if it's joinable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @mmmarinho, I believe that for most scenarios, it would indeed be sufficient. However, I think there's a subtle race condition that the early return, which is based on an atomic operation, helps prevent. Please feel free to correct me if I'm wrong!

Case 1: Checking only if it is joinable()

void stop() {
    running_ = false;
    stop_requested_ = true;
    if (thread_.joinable()) {
        thread_.join();  // Is this safe?
    }
}

Since joinable() and join() are not atomic operations, between the check and the actual join, another thread could slip in and call join() on the same thread.

Case 2: Early return

The early return uses an atomic exchange to ensure only one thread proceeds:

void stop() {
    if (!running_.exchange(false)) {
        return;  // Only the first thread continues
    }
    stop_requested_ = true;
    if (thread_.joinable()) {
        thread_.join();
    }
}

This way, join() is only ever called from a single thread, and the joinable() check remains as an extra safety measure.

Minimal example

I tested a minimal example using both stop() methods (I added the modification in another branch: jazzy_loopancho_no_early_return).

#include <sas_core/sas_thread_manager.hpp>
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

class MyDriver
{
    std::unique_ptr<sas::ThreadManager> tm_;
public:
    MyDriver()
    {
        tm_ = std::make_unique<sas::ThreadManager>(
            "test",
            0.1,
            std::bind(&MyDriver::my_function, this)
            );
        tm_->start();
    }
    void my_function()
    {
        // Simulate some work
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
    void stop()
    {
        tm_->stop();
    }
    bool is_running() const
    {
        return tm_->is_running();
    }
};

int main()
{
    std::cout << "=== Demonstrating Thread Safety in stop() ===" << std::endl;
    std::cout << std::endl;

    MyDriver driver;

    // Give the thread time to start
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    std::cout << "Initial state:" << std::endl;
    std::cout << "  - Thread running: " << (driver.is_running() ? "Yes" : "No") << std::endl;
    std::cout << std::endl;

    std::cout << "Spawning 5 threads that will all call stop() concurrently..." << std::endl;
    std::cout << std::endl;

    std::vector<std::thread> stopping_threads;
    for (int i = 0; i < 5; ++i) {
        stopping_threads.emplace_back([&driver, i]() {
            std::cout << "  - Thread " << i << " (ID: "
                      << std::this_thread::get_id()
                      << ") calling stop()" << std::endl;
            driver.stop();
            std::cout << "  - Thread " << i << " finished stop()" << std::endl;
        });
    }

    // Wait for all stopping threads to complete
    for (auto& t : stopping_threads) {
        t.join();
    }

    std::cout << std::endl;
    std::cout << "Final state:" << std::endl;
    std::cout << "  - Thread running: " << (driver.is_running() ? "Yes" : "No") << std::endl;
    std::cout << std::endl;

    return 0;
}

Compiling with the thread sanitizer

cmake -B build -S . \
  -DCMAKE_CXX_FLAGS="-fsanitize=thread -g -O1 -fno-pie" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread -no-pie" \
  -DCMAKE_POSITION_INDEPENDENT_CODE=OFF

cmake --build build

Output with early return (working ok)

00:17:42: Starting /home/juanjqo/Documents/sas_core_test_examples/thread_manager/minimal_example/build/Desktop_Qt_6_10_1-Debug/minimal_example2...
=== Demonstrating Thread Safety in stop() ===

**************************************************************************
sas::Clock (c) Murilo M. Marinho (murilomarinho.info) 2016-2026 LGPLv3
**************************************************************************
Initial state:
  - Thread running: Yes

Spawning 5 threads that will all call stop() concurrently...

  - Thread 0 (ID: 139925940860608) calling stop()
  - Thread 1 (ID: 139925932467904) calling stop()
  - Thread 1 finished stop()
  - Thread 2 (ID: 139925924075200) calling stop()
  - Thread 2 finished stop()
  - Thread 3 (ID: 139925915682496) calling stop()
  - Thread 3 finished stop()
  - Thread 4 (ID: 139925907289792) calling stop()
  - Thread 4 finished stop()
  - Thread 0 finished stop()

Final state:
  - Thread running: No

00:17:42: The command "/home/juanjqo/Documents/sas_core_test_examples/thread_manager/minimal_example/build/Desktop_Qt_6_10_1-Debug/minimal_example2" finished successfully.

Output with just checking if it is joinable() (not working)

./minimal_example2
=== Demonstrating Thread Safety in stop() ===

**************************************************************************
sas::Clock (c) Murilo M. Marinho (murilomarinho.info) 2016-2026 LGPLv3
**************************************************************************
Initial state:
  - Thread running: Yes

Spawning 5 threads that will all call stop() concurrently...

  - Thread 0 (ID: 137389297096384) calling stop()
  - Thread 1 (ID: 137389288703680) calling stop()
ThreadSanitizer: CHECK failed: sanitizer_thread_registry.cpp:348 "((t)) != (0)" (0x0, 0x0) (tid=133194)
    #0 __tsan::CheckUnwind() ../../../../src/libsanitizer/tsan/tsan_rtl.cpp:675 (libtsan.so.2+0xa9eff) (BuildId: 2a13a7710e361d06f7babbea53065ca2be93f738)
    #1 __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_termination.cpp:86 (libtsan.so.2+0xe5315) (BuildId: 2a13a7710e361d06f7babbea53065ca2be93f738)
    #2 __sanitizer::ThreadRegistry::ConsumeThreadUserId(unsigned long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_thread_registry.cpp:348 (libtsan.so.2+0xe90bc) (BuildId: 2a13a7710e361d06f7babbea53065ca2be93f738)
    #3 pthread_join ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1080 (libtsan.so.2+0x58a44) (BuildId: 2a13a7710e361d06f7babbea53065ca2be93f738)
    #4 std::thread::join() <null> (libstdc++.so.6+0xece32) (BuildId: 753c6c8608b61d4e67be8f0c890e03e0aa046b8b)
    #5 sas::ThreadManager::stop() /home/juanjqo/Documents/sas_core_test_examples/thread_manager/minimal_example/build/_deps/sas_core-src/src/sas_thread_manager.cpp:168 (minimal_example2+0x403b74) (BuildId: 5eb2d5d717e79974dedca30a944e810aff73b163)
    #6 MyDriver::stop() /home/juanjqo/Documents/sas_core_test_examples/thread_manager/minimal_example/example2.cpp:27 (minimal_example2+0x402b5a) (BuildId: 5eb2d5d717e79974dedca30a944e810aff73b163)
    #7 operator() /home/juanjqo/Documents/sas_core_test_examples/thread_manager/minimal_example/example2.cpp:58 (minimal_example2+0x402b5a)
    #8 __invoke_impl<void, main()::<lambda()> > /usr/include/c++/13/bits/invoke.h:61 (minimal_example2+0x402b5a)
    #9 __invoke<main()::<lambda()> > /usr/include/c++/13/bits/invoke.h:96 (minimal_example2+0x402b5a)
    #10 _M_invoke<0> /usr/include/c++/13/bits/std_thread.h:292 (minimal_example2+0x402b5a)
    #11 operator() /usr/include/c++/13/bits/std_thread.h:299 (minimal_example2+0x402b5a)
    #12 _M_run /usr/include/c++/13/bits/std_thread.h:244 (minimal_example2+0x402b5a)
    #13 <null> <null> (libstdc++.so.6+0xecdb3) (BuildId: 753c6c8608b61d4e67be8f0c890e03e0aa046b8b)
    #14 __tsan_thread_start_func ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1012 (libtsan.so.2+0x4edee) (BuildId: 2a13a7710e361d06f7babbea53065ca2be93f738)
    #15 start_thread nptl/pthread_create.c:447 (libc.so.6+0x9cb83) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #16 clone3 ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78 (libc.so.6+0x129d6b) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)

With no sanitizers, the execution stays locked and never ends

00:46:40: Starting /home/juanjqo/Documents/sas_core_test_examples/thread_manager/minimal_example/build/Desktop_Qt_6_10_1-Debug/minimal_example2...
=== Demonstrating Thread Safety in stop() ===

**************************************************************************
sas::Clock (c) Murilo M. Marinho (murilomarinho.info) 2016-2026 LGPLv3
**************************************************************************
Initial state:
  - Thread running: Yes

Spawning 5 threads that will all call stop() concurrently...

  - Thread 0 (ID: 136309414815424) calling stop()
  - Thread 1 (ID: 136309406422720) calling stop()
  - Thread 2 (ID: 136309398030016) calling stop()
  - Thread 3 (ID: 136309389637312) calling stop()
  - Thread 4 (ID: 136309309961920) calling stop()
  - Thread 0 finished stop()

@mmmarinho mmmarinho Aug 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juanjqo I see your point. Yes, you are right.

The pattern I usually saw for this that make the thread-safety somewhat more explicit is using std::lock_guard.

Would it be ok to switch to this mechanism? I think yours is equivalent, i.e., I couldn't think of any case when it fails. The point is that robot drivers are using mutexes so it would be good to keep it consistent.

A major difference I see is that whoever called stop will be blocked until the mutex is released, which might be desirable. If you want it to return early (or have the option to do so), it could have a try_to_lock check with unique_lock.

PS: This is an old memory, it seems that scoped_lock might be better. Please consider it instead.

void stop() {
    std::scoped_lock lock(stop_mutex_); // stop_mutex_ needs to be a member of ThreadManager.
    stop_requested_ = true;
    if (thread_.joinable()) {
        thread_.join();
    }
}

Comment thread src/sas_thread_manager.cpp
Comment thread src/sas_thread_manager.cpp Outdated
@juanjqo
juanjqo marked this pull request as draft August 30, 2026 22:43
@juanjqo
juanjqo marked this pull request as ready for review August 30, 2026 23:52
@juanjqo

juanjqo commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Hi @mmmarinho. I implemented all modifications!

@mmmarinho mmmarinho left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @juanjqo. Some minor additional comments.



// Performance monitoring using sas::Clock
double get_computation_time() const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed anymore? Can we do get_clock().get_time(sas::Clock::Timetype::Computational)?


// Performance monitoring using sas::Clock
double get_computation_time() const;
double get_sleep_time() const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does get_clock() expose this method from sas::Clock?

// Performance monitoring using sas::Clock
double get_computation_time() const;
double get_sleep_time() const;
double get_effective_sampling_time() const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does get_clock() expose this method from sas::Clock?

double get_computation_time() const;
double get_sleep_time() const;
double get_effective_sampling_time() const;
double get_elapsed_time_sec() const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does get_clock() expose this method from sas::Clock?

double get_sleep_time() const;
double get_effective_sampling_time() const;
double get_elapsed_time_sec() const;
long get_overrun_count() const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does get_clock() expose this method from sas::Clock?

const sas::Clock& get_clock() const;

// Statistics from sas::Clock
double get_statistics(const Statistics& statistics,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does get_clock() expose this method from sas::Clock?

void thread_manager::stop()
{
if (!running_.exchange(false)) {
return; // Not running

@mmmarinho mmmarinho Aug 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juanjqo I see your point. Yes, you are right.

The pattern I usually saw for this that make the thread-safety somewhat more explicit is using std::lock_guard.

Would it be ok to switch to this mechanism? I think yours is equivalent, i.e., I couldn't think of any case when it fails. The point is that robot drivers are using mutexes so it would be good to keep it consistent.

A major difference I see is that whoever called stop will be blocked until the mutex is released, which might be desirable. If you want it to return early (or have the option to do so), it could have a try_to_lock check with unique_lock.

PS: This is an old memory, it seems that scoped_lock might be better. Please consider it instead.

void stop() {
    std::scoped_lock lock(stop_mutex_); // stop_mutex_ needs to be a member of ThreadManager.
    stop_requested_ = true;
    if (thread_.joinable()) {
        thread_.join();
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants