modm API documentation
|
Modules | |
Fiber Context Functions | |
Classes | |
class | modm::Fiber< StackSize > |
class | modm::fiber::Scheduler |
class | modm::fiber::Task |
class | modm::fiber::barrier< CompletionFunction > |
class | modm::fiber::counting_semaphore< LeastMaxValue > |
class | modm::fiber::latch |
class | modm::fiber::mutex |
class | modm::fiber::shared_mutex |
Typedefs | |
using | modm::Fiber< StackSize >::id = uintptr_t |
Enums | |
enum | modm::Fiber< StackSize >::Start |
enum | cv_status |
using | modm::Fiber< StackSize >::condition_variable = condition_variable_any |
There is no specialization for std::unique_lock<fiber::mutex> . | |
void | modm::this_fiber::yield () |
static constexpr size_t | modm::Fiber< StackSize >::StackAlignment = sizeof(uintptr_t) * 2 |
Alignment requirements for the bottom and top of the stack. | |
static constexpr size_t | modm::Fiber< StackSize >::StackSizeMinimum = 28 |
Minimum stack size required to push one full fiber context. | |
static constexpr size_t | modm::Fiber< StackSize >::StackSizeDefault = 256 |
lbuild module: modm:processing:fiber
This module provides a lightweight stackful fiber implementation including a simple round-robin scheduler. Here is a minimal example that blinks an LED:
You can construct a fiber from any function without return type or arguments:
To call objects with arguments, wrap the data into a lambda closure and pass it to fiber. The closure will be constructed at the top of the stack and allows the lambda wrapper to call your function with an argument:
Remember to use the right capture method for the lifetime of the objects you want to call. You can std::move()
already constructed objects into the capture, or construct them in the capture directly, if they would get destroyed after fiber construction. You may need to mark the lambda mutable:
modm::Fiber
on the stack! Apart from the general lifetime issues of constructing objects on the stack, the allocated fiber stack size is likely too large for the caller stack and will lead to a stack overflow.A fiber can be passed a modm::fiber::stop_token
to allow the fiber to be stopped cooperatively.
Note that the fiber destructor requests to stop and joins automatically. The interface and behavior is similar to the C++20 std::jthread
.
Fiber are added to the scheduler automatically and start execution when the scheduler is run. You can disable this behavior by setting start
to modm::fiber::Start::Later
during construction and manually starting the fiber when it is ready, also from another fiber:
Fibers can end by returning from their wrapper, after which they will be removed from the scheduler. A fiber can then be restarted again by calling start()
, which will call the closure again from the beginning. Note, that the lambda capture is not destructed and reconstructed, but remains unchanged between restarts. If you need a fiber that is only callable once, you can implement this behavior manually with a boolean in the capture:
The most important customization is the fiber stack size expressed in bytes:
The Fiber
class is intentionally constructed at runtime, so that it does not increase your program size, as the .data
section would. You may also place the fibers into the .faststack
section, which is not zeroed and thus saves a bit of time on startup:
However, it may be desirable to control the placement of the fiber task structure and especially the stack, depending on the types of memories available on your device. This is possible when you construct the stack and task in combination with the modm_section()
macros and its specializations:
The modm::fiber
namespace provides several standard concurrency primitives to synchronize fibers based on the std::thread
interface behavior. Most primitives are implemented on top of <atomic>
, therefore can be called from within (nested) interrupts. The API docs explicitly mention if a function is safe to call from an interrupt.
Task
implements most of the std::jthread
interface.In particular, Task
only implements functionality that does not require dynamic memory allocations. The stack memory needs to be allocated externally and fibers are not movable or copyable and therefore cannot be detached or swapped.
stop_token
and stop_source
with simplified implementations.stop_callback
not implemented.To avoid dynamic memory allocations, a stop_state
object provides the actual memory required for the limited functionality:
Implemented using interrupt-safe atomics.
mutex
and timed_mutex
.recursive_mutex
and recursive_timed_mutex
.shared_mutex
and shared_timed_mutex
.Implemented using interrupt-safe atomics.
lock_guard
, scoped_lock
, unique_lock
and shared_lock
.defer_lock_t
, try_to_lock_t
and adopt_lock_t
.defer_lock
, try_to_lock
and adopt_lock
.try_lock
and lock
.once_flag
and call_once
.Implemented using interrupt-safe atomic flag.
condition_variable
and condition_variable_any
.cv_status
.notify_all_at_thread_exit
not implemented.Notification is implemented as a interrupt-safe 16-bit atomic counter.
counting_semaphore
and binary_semaphore
.Counts are implemented as interrupt-safe 16-bits atomics.
latch
: implemented as interrupt-safe atomics.barrier
: not interrupt-safe!Counts are implemented as 16-bits.
It is difficult to measure stack usage without hardware support, however, detecting stack overflows is simpler with watermarking. A single word at the bottom of the stack is watermarked by default, as a cheap way to detect stack overflows:
Since the stack can also overflow without writing the last word, it is better to measure maximum stack usage and size the stack with a healthy buffer. You must watermark the stack before running the fiber, then you may query the stack usage inside or outside the fiber:
Note that stack usage measurement through watermarking can be inaccurate if the registers contain the watermark value.
On ARMv8-M devices, the PSPLIM register is set to the bottom of the fiber stack so that stack overflows are reliably detected and cause a STKOF UsageFault if enabled, or a HardFault if not, both on the main stack.
Currently no recovery strategy is implementable, since accessing the scheduler is not interrupt-safe and any acquired resources of the offending fiber are not tracked and can thus also not be released. Therefore, no default implementation to handle the UsageFault is provided.
Here is an example handler if you wish to experiment with solutions:
The scheduler run()
function will suspend execution of the call site, usually the main function, start each fiber and continue to execute them until they all ended and then return execution to the call site:
Please note that neither the fiber nor scheduler is interrupt safe, so starting threads from interrupt context is a bad idea!
yield()
outside of a fiber If yield()
is called before the scheduler started or if only one fiber is running, it simply returns in-place, since there is nowhere to switch to.Fibers are implemented by saving callee registers to the current stack, then switching to a new stack and restoring callee registers from this stack. The static modm::this_fiber::yield()
function wraps this functionality in a transparent way.
On AVRs the fiber stack is shared with the currently active interrupt. This requires the fiber stack size to include the worst case stack size of all interrupts. Fortunately on AVRs interrupts cannot be nested.
On Cortex-M, the main function is entered using the MSP in Handler mode. After calling modm::fiber::Scheduler::run()
the PSP is used as a Fiber stack pointer in Thread mode. Therefore all interrupts are using the main stack whose size is defined by the modm:platform:cortex-m:main_stack_size
option and will not increase the fiber stack size at all.
When using this module in combination with the modm:platform:multicore
module, each core gets its own fiber scheduler, which will internally be selected based on the CPU ID. Since the scheduler is not thread-safe, you cannot add fibers from one core to the other. Instead you must construct the fiber without starting it, and when executing on the other core, start()
it in that context.
Here is an example for the RP2040 device, which additionally allocates the stack and task into the core-affine memory:
using modm::Fiber< StackSize >::id = uintptr_t |
Identifier of a fiber task.
|
strong |
The Fiber scheduling policy.
void modm::this_fiber::yield | ( | ) |
Calls into the currently active scheduler to jump to the next fiber. Local control flow resumes by returning from this function.
|
staticconstexpr |
The default stack size is estimated experimentally so that a fiber can use modm::IOStream
to log out information, which is fairly stack intensive. Use modm::fiber::Task::stack_usage()
to determine the real stack usage.