Skip to content

Commit

Permalink
Merge pull request #141 from cactusdynamics/example-updates
Browse files Browse the repository at this point in the history
Update examples with readme and latest style
  • Loading branch information
shuhaowu authored Oct 14, 2024
2 parents 3d569cf + 0da42a8 commit e086208
Show file tree
Hide file tree
Showing 20 changed files with 179 additions and 69 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ writing a real-time Linux application. Some key features are:
Examples
--------

Core examples that show you how to use all the facilities of cactus-rt:

* [`simple_example`](examples/simple_example/): The most basic example showing
a single real-time looping thread running at 1000 Hz.
* [`tracing_example`](examples/tracing_example/): This demonstrates how to use
Expand All @@ -38,14 +40,26 @@ Examples
priority-inheritence mutex (`cactus_rt::mutex`) to pass data between real-time
and non-real-time threads via the implementation of a mutex-based double
buffer.
* [`lockless_examples`](examples/lockless_examples/): Demonstrates the usage of
lockless data structures built into cactus-rt to safely pass data to and from
the real-time thread.
* [`logging_example`](examples/logging_example/): Demonstrates setting up custom
logging configuration via `cactus_rt::App`.
* [`message_passing_example`](examples/message_passing_example/): A simplified
example of a real robotics application consists of two threads: a real-time
thread that is generating metrics and a non-real-time data logger that logs
the metrics to disk.
* [`ros2`](examples/ros2/): Shows how to use cactus-rt with ROS2.

Some examples showing optional/advanced features/usage patterns of cactus-rt.

* [`simple_deadline_example`](examples/simple_deadline_example/): Same as
`simple_example`, except it uses `SCHED_DEADLINE` as opposed to `SCHED_FIFO`.
This is for a more advanced use case.

* [`tracing_example_no_rt`](examples/tracing_example_no_rt/): Shows how to using the tracing library in `cactus_rt` without using `cactus_rt::App`.
* [`random_example`](examples/random_example/): Shows you how to use cactus-rt's
real-time-safe random number generator.
* [`tracing_example_no_rt`](examples/tracing_example_no_rt/): Shows how to using
the tracing library in `cactus_rt` without using `cactus_rt::App`.


Installation instructions
Expand Down
7 changes: 7 additions & 0 deletions examples/lockless_examples/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
Lockless examples
=================

`realtime_read`
---------------

This example shows you how to use the `RealtimeReadableValue` lockless data
structure. This data structure allows you to read a struct from a single
real-time thread in a wait-free manner O(1). The writer has to be a single
thread is non-real-time as the write algorithm is lock-free but not wait-free.
8 changes: 4 additions & 4 deletions examples/lockless_examples/realtime_read.cc
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class RTThread : public CyclicThread {
Context& ctx_;
Pose current_target_pose_;

static cactus_rt::CyclicThreadConfig CreateThreadConfig() {
static cactus_rt::CyclicThreadConfig MakeConfig() {
cactus_rt::CyclicThreadConfig thread_config;
thread_config.period_ns = 1'000'000;
thread_config.cpu_affinity = std::vector<size_t>{2};
Expand All @@ -70,7 +70,7 @@ class RTThread : public CyclicThread {
}

public:
RTThread(Context& ctx) : CyclicThread("RTThread", CreateThreadConfig()), ctx_(ctx) {}
RTThread(Context& ctx) : CyclicThread("RTThread", MakeConfig()), ctx_(ctx) {}

protected:
LoopControl Loop(int64_t /*now*/) noexcept final {
Expand Down Expand Up @@ -104,14 +104,14 @@ class RTThread : public CyclicThread {
class NonRTThread : public Thread {
Context& ctx_;

static cactus_rt::ThreadConfig CreateThreadConfig() {
static cactus_rt::ThreadConfig MakeConfig() {
cactus_rt::ThreadConfig thread_config;
thread_config.SetOtherScheduler();
return thread_config;
}

public:
NonRTThread(Context& ctx) : Thread("NonRTThread", CreateThreadConfig()), ctx_(ctx) {}
NonRTThread(Context& ctx) : Thread("NonRTThread", MakeConfig()), ctx_(ctx) {}

void Run() final {
ctx_.target_pose.Write(Pose(1.5, 2.5, 3.5, 4.5, 5.5, 6.5));
Expand Down
27 changes: 14 additions & 13 deletions examples/logging_example/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@ using cactus_rt::CyclicThread;
class ExampleRTThread : public CyclicThread {
int64_t loop_counter_ = 0;

static cactus_rt::CyclicThreadConfig MakeConfig() {
cactus_rt::CyclicThreadConfig thread_config;
thread_config.period_ns = 1'000'000;
thread_config.cpu_affinity = std::vector<size_t>{2};
thread_config.SetFifoScheduler(80);
return thread_config;
}

public:
ExampleRTThread(const char* name, cactus_rt::CyclicThreadConfig config) : CyclicThread(name, config) {}
ExampleRTThread() : CyclicThread("ExampleRTThread", MakeConfig()) {}

int64_t GetLoopCounter() const {
return loop_counter_;
Expand All @@ -26,33 +34,26 @@ class ExampleRTThread : public CyclicThread {
if (loop_counter_ % 1000 == 0) {
LOG_INFO(Logger(), "Loop {}", loop_counter_);
}

LOG_INFO_LIMIT(std::chrono::milliseconds{1500}, Logger(), "Log limit: Loop {}", loop_counter_);
return LoopControl::Continue;
}
};

int main() {
cactus_rt::CyclicThreadConfig thread_config;
thread_config.period_ns = 1'000'000;
thread_config.cpu_affinity = std::vector<size_t>{2};
thread_config.SetFifoScheduler(80);

// Create a cactus_rt app configuration
cactus_rt::AppConfig app_config;

// Create a Quill logging config to configure logging
quill::Config logging_config;

// Disable strict timestamp order - this will be faster, but logs may appear out of order
logging_config.backend_thread_strict_log_timestamp_order = false;
app_config.logger_config.backend_thread_strict_log_timestamp_order = false;

// Set the background logging thread CPU affinity
logging_config.backend_thread_cpu_affinity = 1; // Different CPU than the CyclicThread CPU!
app_config.logger_config.backend_thread_cpu_affinity = 1; // Different CPU than the CyclicThread CPU!

app_config.logger_config = logging_config;
App app("LoggingExampleApp", app_config);

auto thread = app.CreateThread<ExampleRTThread>("ExampleRTThread", thread_config);
auto thread = app.CreateThread<ExampleRTThread>();

constexpr unsigned int time = 5;

std::cout << "Testing RT loop for " << time << " seconds.\n";
Expand Down
25 changes: 8 additions & 17 deletions examples/message_passing_example/data_logger_thread.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ DataLogger::DataLogger(
period_ns_(period_ns),
write_data_interval_ns_(write_data_interval_ns),
queue_(kQueueCapacity),
message_count_({0, 0}) {
message_count_(CountData{0, 0}) {
file_.open(data_file_path);
if (!file_.is_open()) {
throw std::runtime_error{"failed to open data file"};
Expand Down Expand Up @@ -61,10 +61,6 @@ void DataLogger::Run() {
break;
}

// TODO: looking at the code for this, it seems safe as it just calls
// nanosleep. That said, to make it explicit that we're not using the c++
// thread library, should just refactor that code out in to Thread::sleep
// here.
std::this_thread::sleep_for(std::chrono::nanoseconds(period_ns_));
}
}
Expand All @@ -87,22 +83,17 @@ void DataLogger::WriteAndEmptyDataFromBuffer() noexcept {
}

void DataLogger::ReadAndLogMessageCount() noexcept {
const auto current_count = message_count_.load();
const auto current_count = message_count_.Read();

LOG_INFO(Logger(), "received {} messages and dropped {}", current_count.total_messages, current_count.total_messages - current_count.successful_messages);
}

// A demonstration of how to pass a small amount of data via std::atomic if it can be done in a lock free manner.
// This is called from the real-time thread. The loop is probably unnecessary
// because it must be the only thread that reads and writes to this variable..
// successful_message_count should be either 0 or 1...
// This is called from the real-time thread.
void DataLogger::IncrementMessageCount(uint32_t successful_message_count) noexcept {
auto old_count = message_count_.load();
decltype(old_count) new_count;

do {
new_count = old_count;
new_count.successful_messages += successful_message_count;
new_count.total_messages += 1;
} while (!message_count_.compare_exchange_weak(old_count, new_count));
message_count_.Modify([successful_message_count](CountData old_value) noexcept {
old_value.successful_messages += successful_message_count;
old_value.total_messages += 1;
return old_value;
});
}
5 changes: 2 additions & 3 deletions examples/message_passing_example/data_logger_thread.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#ifndef CACTUS_RT_EXAMPLES_MESSAGE_PASSING_EXAMPLE_DATA_LOGGER_H_
#define CACTUS_RT_EXAMPLES_MESSAGE_PASSING_EXAMPLE_DATA_LOGGER_H_

#include <cactus_rt/experimental/lockless.h>
#include <cactus_rt/rt.h>
#include <readerwriterqueue.h>

#include <atomic>
#include <cstdint>
#include <fstream>
#include <vector>
Expand Down Expand Up @@ -38,9 +38,8 @@ class DataLogger : public Thread {
uint32_t total_messages;
};

using AtomicMessageCount = std::atomic<CountData>;
using AtomicMessageCount = cactus_rt::experimental::lockless::AtomicMessage<CountData>;
AtomicMessageCount message_count_;
static_assert(AtomicMessageCount::is_always_lock_free);

std::vector<OutputData> data_buffer_; // A simple buffer to hold the data in non-realtime thread so batch writing can occur

Expand Down
4 changes: 2 additions & 2 deletions examples/message_passing_example/rt_thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class RtThread : public CyclicThread {
size_t max_iterations_;
size_t iterations_ = 0;

static cactus_rt::CyclicThreadConfig MakeRealTimeThreadConfig() {
static cactus_rt::CyclicThreadConfig MakeConfig() {
cactus_rt::CyclicThreadConfig config;
config.period_ns = 1'000'000;
config.cpu_affinity = std::vector<size_t>{2};
Expand All @@ -25,7 +25,7 @@ class RtThread : public CyclicThread {

public:
RtThread(std::shared_ptr<DataLogger> data_logger, size_t max_iterations = 30000)
: CyclicThread("RtThread", MakeRealTimeThreadConfig()),
: CyclicThread("RtThread", MakeConfig()),
data_logger_(data_logger),
max_iterations_(max_iterations) {
}
Expand Down
5 changes: 4 additions & 1 deletion examples/mutex_example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ thread writes to the double buffer at 1 kHz and the non-RT thread reads it every
half second. Once it is read, it is logged via the `cactus_rt` logging
capability.

_Note: a lockless version of this double buffer is implemented by the cactus-rt framework under `cactus_rt::experimental::lockless::spsc::AtomicWritableValue` which doesn't require a lock. That serves as an alternative to this code without the usage of a mutex._
_Note: a lockless version of this double buffer is implemented by the cactus-rt
framework under `cactus_rt::experimental::lockless::spsc::RealtimeWritableValue`
which doesn't require a lock. That serves as an alternative to this code without
the usage of a mutex._

To run:

Expand Down
10 changes: 6 additions & 4 deletions examples/mutex_example/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ using cactus_rt::Thread;
// This is the data structure we are passing between the RT and non-RT thread.
// It is big enough such that it cannot be atomically changed with a single
// instruction to necessitate a mutex.
//
// Alternatively, you can also use RealtimeWritableValue instead of a mutex.
struct Data {
double v1 = 0.0;
double v2 = 0.0;
Expand All @@ -25,7 +27,7 @@ class RTThread : public CyclicThread {

public:
explicit RTThread(NaiveDoubleBuffer<Data>& buf)
: CyclicThread("RTThread", CreateConfig()),
: CyclicThread("RTThread", MakeConfig()),
buf_(buf) {}

protected:
Expand All @@ -45,7 +47,7 @@ class RTThread : public CyclicThread {
}

private:
static cactus_rt::CyclicThreadConfig CreateConfig() {
static cactus_rt::CyclicThreadConfig MakeConfig() {
cactus_rt::CyclicThreadConfig thread_config;
thread_config.period_ns = 1'000'000;
thread_config.SetFifoScheduler(80);
Expand All @@ -59,7 +61,7 @@ class NonRTThread : public Thread {

public:
explicit NonRTThread(NaiveDoubleBuffer<Data>& buf)
: Thread("NonRTThread", CreateConfig()),
: Thread("NonRTThread", MakeConfig()),
buf_(buf) {}

protected:
Expand All @@ -73,7 +75,7 @@ class NonRTThread : public Thread {
}

private:
static cactus_rt::ThreadConfig CreateConfig() {
static cactus_rt::ThreadConfig MakeConfig() {
cactus_rt::CyclicThreadConfig rt_thread_config;
rt_thread_config.SetOtherScheduler(0 /* niceness */);
return rt_thread_config;
Expand Down
13 changes: 13 additions & 0 deletions examples/random_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
`random_example`
================

Technically, [none of the random number generators in C++ are O(1)][1]. This
means there is a small (infinitesimal?) probability that it may cause real-time
problems (for some of the implementation it may be near impossible). To ensure
absolute safety at the cost of slightly non-uniform random number generation,
cactus-rt implements a O(1) random number generator with the Xorshift algorithm.
This example shows how to use it.

**WARNING: DO NOT USE THIS RNG IN SECURE CRYPTOGRAPHIC CONTEXT AS IT IS NOT PERFECTLY RANDOM**.

[1]: https://youtu.be/Tof5pRedskI?t=2514
7 changes: 5 additions & 2 deletions examples/random_example/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ class Histogram {
};

int main() {
// Generate the seed once in non-real-time code.
const uint64_t seed = std::random_device{}();
std::cout << "Seed: " << seed << "\n";

Histogram<20> hist;

cactus_rt::experimental::Xorshift64Rand rng(seed);
// Initialize the RNG state in non-real-time code.
cactus_rt::experimental::random::Xorshift64Rand rng(seed);

for (int i = 0; i < 1'000'000; i++) {
const float num = cactus_rt::experimental::RandomRealNumber(rng);
// RealNumber(rng) is always O(1).
const float num = cactus_rt::experimental::random::RealNumber(rng);
if (num >= 1.0F || num < 0.0F) {
std::cerr << "ERROR: seed = " << seed << " i = " << i << " num = " << num << " is out of range \n";
return 1;
Expand Down
Loading

0 comments on commit e086208

Please sign in to comment.