manticore
Main thread executor
Loading...
Searching...
No Matches
Main thread function executor

Unit tests Documentation

Overview

This repository implements a C++ class that allows worker threads to pass functions for execution on the main thread. It is intended for non-thread-safe third-party code where locking is insufficient, e.g., due to garbage collection. For example, we can use manticore to allow workers to execute R code that might trigger GC. This is useful for parallelizing functions that need to occasionally - but safely - call the R APIs.

Quick start

The manticore function is based around the Executor class, which should be used like below:

#include <thread>
#include <vector>
// For a given number of threads...
mexec.initialize(nthreads);
std::vector<std::thread> jobs;
jobs.reserve(nthreads);
for (int t = 0; t < nthreads; ++t) {
jobs.emplace_back([&](int thread) -> void {
mexec.run([&]() -> void { /* do something on the main thread */ });
// now do something in the worker thread
mexec.run([&]() -> void { /* do another thing on the main thread */ });
// and so on, until...
mexec.finish_thread();
}, t);
}
mexec.listen();
for (auto& j : jobs) {
j.join();
}
Execute arbitrary functions on the main thread.
Definition manticore.hpp:33
void listen()
Definition manticore.hpp:157
void run(Function_ f)
Definition manticore.hpp:115
void finish_thread(bool notify=true)
Definition manticore.hpp:83
void initialize(size_t n, std::string e)
Definition manticore.hpp:58
Defines the manticore namespace.

The above code initializes the Executor and launches worker threads that request main thread execution via run(). Meanwhile, the main thread is listening for worker requests via listen(). This blocks until all workers call finish_thread(), at which point the main thread is allowed to proceed.

Check out the reference documentation for more details.

Create a global <tt>Executor</tt>

Sometimes, the Executor::run() function needs to be called from deep inside another library, with no opportunity to pass the actual Executor object through the library's interface. (Looking at you, tatami_r.) In such cases, we should create a global Executor object that can be called from anywhere. For standard source files, we can use extern linkage, while for header-only libraries, we can use static getters:

auto& executor() {
static manticore::Executor mexec;
return mexec;
}

This allows us to do:

void some_function() {
// Do various things.
auto& mexec = executor();
mexec.run([&]() -> void { /* Something that must be done on the main thread. */ });
// More things.
}

some_function() can now be called inside a parallel context:

auto& mexec = executor();
mexec.initialize(2);
for (int t = 0; t < nthreads; ++t) {
jobs.emplace_back([&](int thread) -> void {
some_function();
mexec.finish_thread();
}, t);
}
mexec.listen();
for (auto& j : jobs) {
j.join();
}

Note that some_function() can also be called outside of a parallel context, i.e. without running initialize(), finish_thread() or listen(). In such cases, it will run directly on the main thread, allowing developers to re-use the same function in serial applications.

Building projects

CMake with <tt>FetchContent</tt>

If you're using CMake, you just need to add something like this to your CMakeLists.txt:

include(FetchContent)
FetchContent_Declare(
manticore
GIT_REPOSITORY https://github.com/tatami-inc/manticore
GIT_TAG master # or any version of interest
)
FetchContent_MakeAvailable(manticore)

Then you can link to manticore to make the headers available during compilation:

# For executables:
target_link_libraries(myexe manticore)
# For libaries
target_link_libraries(mylib INTERFACE manticore)

CMake using <tt>find_package()</tt>

You can install the library by cloning a suitable version of this repository and running the following commands:

mkdir build && cd build
cmake ..
cmake --build . --target install

Then you can use find_package() as usual:

find_package(tatami_manticore CONFIG REQUIRED)
target_link_libraries(mylib INTERFACE tatami::manticore)

Manual

If you're not using CMake, the simple approach is to just copy the files - either directly or with Git submodules - and include their path during compilation with, e.g., GCC's -I.