pw_thread#
The pw_thread
module contains utilities for thread creation and thread
execution.
Thread Sleeping#
C++#
-
void pw::this_thread::sleep_for(chrono::SystemClock::duration sleep_duration)#
Blocks the execution of the current thread for at least the specified duration. This function may block for longer due to scheduling or resource contention delays.
A sleep duration of 0 will at minimum yield, meaning it will provide a hint to the implementation to reschedule the execution of threads, allowing other threads to run.
Precondition: This can only be called from a thread, meaning the scheduler is running.
-
void pw::this_thread::sleep_until(chrono::SystemClock::time_point wakeup_time)#
Blocks the execution of the current thread until at least the specified time has been reached. This function may block for longer due to scheduling or resource contention delays.
A sleep deadline in the past up to the current time will at minimum yield meaning it will provide a hint to the implementation to reschedule the execution of threads, allowing other threads to run.
Precondition: This can only be called from a thread, meaning the scheduler is running.
Examples in C++#
#include <chrono>
#include "pw_chrono/system_clock.h"
#include "pw_thread/sleep.h"
using std::literals::chrono_literals::ms;
void FunctionInvokedByThread() {
pw::this_thread::sleep_for(42ms);
}
void AnotherFunctionInvokedByThread() {
pw::this_thread::sleep_until(pw::chrono::SystemClock::now() + 42ms);
}
C#
-
void pw_this_thread_SleepFor(pw_chrono_SystemClock_Duration sleep_duration)#
Invokes
pw::this_thread::sleep_until(sleep_duration)
.
-
void pw_this_thread_SleepUntil(pw_chrono_SystemClock_TimePoint wakeup_time)#
Invokes
pw::this_thread::sleep_until(wakeup_time)
.
Thread Yielding#
C++#
-
void pw::this_thread::yield() noexcept#
Provides a hint to the implementation to reschedule the execution of threads, allowing other threads to run.
The exact behavior of this function depends on the implementation, in particular on the mechanics of the OS scheduler in use and the state of the system.
Precondition: This can only be called from a thread, meaning the scheduler is running.
Example in C++#
#include "pw_thread/yield.h"
void FunctionInvokedByThread() {
pw::this_thread::yield();
}
C#
-
void pw_this_thread_Yield(void)#
Invokes
pw::this_thread::yield()
.
Thread Identification#
The class pw::Thread::id
is a lightweight, trivially copyable class
that serves as a unique identifier of Thread objects.
Instances of this class may also hold the special distinct value that does not represent any thread. Once a thread has finished, the value of its Thread::id may be reused by another thread.
This class is designed for use as key in associative containers, both ordered and unordered.
Although the current API is similar to C++11 STL std::thread::id, it is missing the required hashing and streaming operators and may diverge further in the future.
A thread’s identification (pw::Thread::id
) can be acquired only in
C++ in one of two ways:
Using the
pw::Thread
handle’spw::Thread::id get_id() const
method.While executing the thread using
pw::Thread::id pw::this_thread::get_id() noexcept
.
-
pw::Thread::id pw::this_thread::get_id() noexcept#
This is thread safe, not IRQ safe. It is implementation defined whether this is safe before the scheduler has started.
Example#
#include "pw_thread/thread.h"
void FunctionInvokedByThread() {
const pw::Thread::id my_id = pw::this_thread::get_id();
}
Thread creation#
The pw::Thread
class can be used to create a thread, allowing
multiple functions to execute concurrently.
API reference#
-
using pw::Thread = ::pw::thread::Thread#
pw::thread::Thread
will be renamed topw::Thread
. New code should refer topw::Thread
.
-
class Thread#
The class
Thread
can represent a single thread of execution. Threads allow multiple functions to execute concurrently.Threads may begin execution immediately upon construction of the associated thread object (pending any OS scheduling delays), starting at the top-level function provided as a constructor argument. The return value of the top-level function is ignored. The top-level function may communicate its return value by modifying shared variables (which may require synchronization, see
pw_sync
andstd::atomic
)Thread
objects may also be in the state that does not represent any thread (after default construction, move from, detach, or join), and a thread of execution may be not associated with any thread objects (after detach).No two
Thread
objects may represent the same thread of execution;Thread
is not CopyConstructible or CopyAssignable, although it is MoveConstructible and MoveAssignable.Public Types
-
using native_handle_type = backend::NativeThreadHandle#
The type of the native handle for the thread. As with
std::thread
, use is inherently non-portable.
-
using id = ::pw::thread::backend::NativeId#
The class id is a lightweight, trivially copyable class that serves as a unique identifier of Thread objects.
Instances of this class may also hold the special distinct value that does not represent any thread. Once a thread has finished, the value of its
Thread::id
may be reused by another thread.This class is designed for use as key in associative containers, both ordered and unordered.
The backend must ensure that:
There is a default construct which does not represent a thread.
Compare operators (
==
,!=
,<
,<=
,>
,>=
) are provided to compare and sort IDs.
Public Functions
-
Thread()#
Creates a new thread object which does not represent a thread of execution yet.
-
Thread(const Options &options, Function<void()> &&entry)#
Creates a thread from a void-returning function or lambda.
This function accepts any callable (including lambdas) which returns
void
. When using a lambda, the captures must not exceed the inline size ofpw::Function
(usually a single pointer) unless dynamic allocation is enabled.To invoke a member method of a class a static lambda closure can be used to ensure the dispatching closure is not destructed before the thread is done executing. For example:
class Foo { public: void DoBar() {} }; Foo foo; // Now use the lambda closure as the thread entry, passing the foo's // this as the argument. Thread thread(options, [&foo]() { foo.DoBar(); }); thread.detach();
- Post:
The thread get EITHER detached or joined.
-
Thread(const Options &options, ThreadCore &thread_core)#
Creates a thread from a
ThreadCore
subclass.ThreadCore
is not recommended for new code; use thepw::Function<void()>
constructor instead.For example:
class Foo : public ThreadCore { private: void Run() override {} }; Foo foo; // Now create the thread, using foo directly. Thread(options, foo).detach();
- Post:
The thread get EITHER detached or joined.
-
Thread &operator=(Thread &&other)#
- Post:
The other thread no longer represents a thread of execution.
-
~Thread()#
- Pre:
The thread must have been EITHER detached or joined.
-
id get_id() const#
Returns a value of
Thread::id
identifying the thread associated with*this
. If there is no thread associated, default constructedThread::id
is returned.
-
inline bool joinable() const#
Checks if the
Thread
object identifies an active thread of execution which has not yet been detached. Specifically, returns true ifget_id() != pw::Thread::id()
anddetach()
has NOT been invoked. So a default constructed thread is not joinable and neither is one which was detached.A thread that has not started or has finished executing code which was never detached, but has not yet been joined is still considered an active thread of execution and is therefore joinable.
-
void detach()#
Separates the thread of execution from the thread object, allowing execution to continue independently. Any allocated resources will be freed once the thread exits.
- Pre:
The thread must have been NEITHER detached nor joined.
- Post:
After calling detach *this no longer owns any thread.
-
native_handle_type native_handle()#
Returns the native handle for the thread. As with
std::thread
, use is inherently non-portable.
-
using native_handle_type = backend::NativeThreadHandle#
-
class Options#
The Options contains the parameters needed for a thread to start.
Options are backend specific and ergo the generic base class cannot be directly instantiated.
The attributes which can be set through the options are backend specific but may contain things like the thread name, priority, scheduling policy, core/processor affinity, and/or an optional reference to a pre-allocated Context (the collection of memory allocations needed for a thread to run).
Options shall NOT have an attribute to start threads as detached vs joinable. All
pw::Thread
instances must be explicitlyjoin()
’d ordetach()
’d through the run-time Thread API.Note that if backends set
PW_THREAD_JOINING_ENABLED
to false, backends may use native OS specific APIs to create native detached threads because thejoin()
API would be compiled out. However, users must still explicitly invokedetach()
.Options must not contain any memory needed for a thread to run (TCB, stack, etc.). The Options may be deleted or re-used immediately after starting a thread.
Differences from std::thread
#
The pw::thread:Thread
API is similar to the C++11 STL std::thread class, meaning the object is
effectively a thread handle and not an object which contains the thread’s
context. Unlike std::thread
, the API requires pw::thread::Options
as an
argument. These options are platform-specific, and allow the user to specify
details such as the thread’s name, priority, stack size, and where the thread’s
memory will be stored.
We recognize that the C++11’s STL std::thread
API has some drawbacks where
it is easy to forget to join or detach the thread handle. Because of this, we
offer helper wrappers like the pw::thread::DetachedThread
. Soon we will
extend this by also adding a pw::thread::JoiningThread
helper wrapper which
will also have a lighter weight C++20 std::jthread
like cooperative
cancellation contract to make joining safer and easier.
Execution order#
Threads may begin execution immediately upon construction of the associated thread object (pending any OS scheduling delays), starting at the top-level function provided as a constructor argument. The top-level function may communicate its return value by modifying shared variables (which may require synchronization, see pw_sync)
Thread objects may also be in the state that does not represent any thread (after default construction, move from, detach, or join), and a thread of execution may be not associated with any thread objects (after detach).
No two Thread objects may represent the same thread of execution; Thread is not CopyConstructible or CopyAssignable, although it is MoveConstructible and MoveAssignable.
Supported on |
Backend module |
FreeRTOS |
|
ThreadX |
|
embOS |
|
STL |
|
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
Module Configuration Options#
The following configurations can be adjusted via compile-time configuration of this module, see the module documentation for more details.
-
PW_THREAD_CONFIG_LOG_LEVEL#
The log level to use for this module. Logs below this level are omitted.
Options#
The pw::thread::Options
contains the parameters or attributes needed for a
thread to start.
Pigweed does not generalize options, instead we strive to give you full control where we provide helpers to do this.
Options are backend specific and ergo the generic base class cannot be directly instantiated.
The attributes which can be set through the options are backend specific but may contain things like the thread name, priority, scheduling policy, core/processor affinity, and/or an optional reference to a pre-allocated Context (the collection of memory allocations needed for a thread to run).
Options shall NOT have an attribute to start threads as detached vs joinable.
All pw::Thread
instances must be explicitly join()
’d or
detach()
’d through the run-time Thread API.
Note that if backends set PW_THREAD_JOINING_ENABLED
to false, backends
may use native OS specific APIs to create native detached threads because the
join()
API would be compiled out. However, users must still explicitly
invoke detach()
.
Options must not contain any memory needed for a thread to run (TCB, stack, etc.). The Options may be deleted or re-used immediately after starting a thread.
Please see the thread creation backend documentation for how their Options work.
Portable Thread Creation#
Due to the fact that pw::thread::Options
cannot be created in portable code,
some extra work must be done in order to permit portable thread creation.
Namely, a reference to the portable pw::thread::Options
base class interface
must be provided through a header or extern which points to an instantiation in
non-portable code.
This can be most easily done through a facade and set of backends. This approach can be powerful; enabling multithreaded unit/integration testing which can run on both the host and on a device with the device’s exact thread options.
Alternatively, it can also be be injected at build time by instantiating backend specific build rule which share the same common portable source file(s) but select backend specific source files and/or dependencies which provide the non-portable option instantiations.
As an example, let’s say we want to create a thread on the host and on a device
running FreeRTOS. They could use a facade which contains a threads.h
header
with the following contents:
// Contents of my_app/threads.h
#pragma once
#include "pw_thread/thread.h"
namespace my_app {
const pw::thread::Options& HellowWorldThreadOptions();
} // namespace my_app
This could then be backed by two different backend implementations based on
the thread backend. For example for the STL the backend’s stl_threads.cc
source file may look something like:
// Contents of my_app/stl_threads.cc
#include "my_app/threads.h"
#include "pw_thread_stl/options.h"
namespace my_app {
const pw::thread::Options& HelloWorldThreadOptions() {
static constexpr auto options = pw::thread::stl::Options();
return options;
}
} // namespace my_app
While for FreeRTOS the backend’s freertos_threads.cc
source file may look
something like:
// Contents of my_app/freertos_threads.cc
#include "FreeRTOS.h"
#include "my_app/threads.h"
#include "pw_thread_freertos/context.h"
#include "pw_thread_freertos/options.h"
#include "task.h"
namespace my_app {
StaticContextWithStack<kHelloWorldStackWords> hello_world_thread_context;
const pw::thread::Options& HelloWorldThreadOptions() {
static constexpr auto options =
pw::thread::freertos::Options()
.set_name("HelloWorld")
.set_static_context(hello_world_thread_context)
.set_priority(kHelloWorldThreadPriority);
return options;
}
} // namespace my_app
Detaching & Joining#
The Thread::detach()
API is always available, to let you separate the
thread of execution from the thread object, allowing execution to continue
independently.
The joining API, more specifically Thread::join()
, is conditionally
available depending on the selected backend for thread creation and how it is
configured. The backend is responsible for providing the
PW_THREAD_JOINING_ENABLED
macro through
pw_thread_backend/thread_native.h
. This ensures that any users which include
pw_thread/thread.h
can use this macro if needed.
Please see the selected thread creation backend documentation for how to enable joining if it’s not already enabled by default.
Warning
A constructed pw::Thread
which represents a thread of execution
must be EITHER detached or joined, else the destructor will assert!
DetachedThread#
To make it slightly easier and cleaner to spawn detached threads without having
to worry about thread handles, a wrapper DetachedThread()
function is
provided which creates a Thread
and immediately detaches it. For example
instead of:
Thread(options, foo).detach();
You can instead use this helper wrapper to:
DetachedThread(options, foo);
The arguments are directly forwarded to the Thread constructor and ergo exactly match the Thread constuctor arguments for creating a thread of execution.
Thread functions and ThreadCore#
Thread functions may be provided using either a pw::Function<void()>
(which may be a lambda or function pointer) or an implementation of the
pw::thread::ThreadCore
interface.
To use the pw::Function<void()>
interface, provide a no-argument,
void-returning lambda or other callable:
Thread thread(options, []() {
// do some work in a thread.
});
Note that lambdas can capture up to one pointer-sized argument (or more if dynamic allocation is enabled). This can be used to call methods on existing objects (though be sure that the objects’ lifetime will outlive the thread, and note that synchronization may be needed).
class Foo {
public:
void DoBar() {}
};
Foo foo;
Thread thread(options, [&foo] {
foo.DoBar();
});
Alternatively, you can extend the ThreadCore
class in order to use a more
explicit construction. For example:
class Foo : public ThreadCore {
private:
void Run() override {}
};
Foo foo;
// Now create the thread, using foo directly.
Thread(options, foo).detach();
Warning
Because the thread may start after the pw::Thread
creation, an
object which implements the ThreadCore MUST meet or exceed the lifetime of
its thread of execution!
Unit testing with threads#
-
class TestThreadContext#
TestThreadContext
is a facade class for creating threads for unit tests in a platform independent way. To use it, setpw_thread_TEST_THREAD_CONTEXT_BACKEND
to a backend that implements thepw::thread::test::backend::TestThreadContextNative
class.To create a thread for unit testing, instantiate a
TestThreadContext
, then calloptions()
to obtain apw::thread::Options
. Use thatOptions
to start aThread
. Users must ensure the context’s lifespan outlives the thread it creates. Recycling or destroying the context is only allowed ifjoin()
is called on the thread first.pw::thread::test::TestThreadContext context; pw::Thread test_thread(context.options(), ExampleThreadFunction);
Threads created with
TestThreadContext
cannot be configured in any way. Backends should create threads with sufficient resources to execute typical unit tests. Tests for complex scenarios or interactions where e.g. priority matters are not portable, andTestThreadContext
may not work for them. Non-portable tests may include backend-specific headers and instantiate thread options for their platforms as required.Note
Developers should structure their logic so it can be tested without spawning a thread. Unit tests should avoid spawning threads unless absolutely necessary.
Warning
Threads using the
TestThreadContext
may only be detached if the context has a static lifetime, meaning the context is both never re-used and not destroyed before the end of the lifetime of the application.Public Functions
-
inline const Options &options() const#
pw::thread::test::TestThreadContext
returns apw::thread::Options
associated with the this object, which can be used to contruct a thread.- Returns:
The default options for testing thread.
-
inline const Options &options() const#
As an example, the STL TestThreadContext
backend implementation in
test_thread_context_native.h
is shown below.
namespace pw::thread::test::backend {
// Native Test Thread Options backend class for the C++ standard library.
class TestThreadContextNative {
public:
constexpr TestThreadContextNative() = default;
TestThreadContextNative(const TestThreadContextNative&) = delete;
TestThreadContextNative& operator=(const TestThreadContextNative&) = delete;
~TestThreadContextNative() = default;
const Options& options() const { return options_; }
private:
stl::Options options_;
};
} // namespace pw::thread::test::backend
Thread Iteration#
C++#
-
Status ForEachThread(const ThreadCallback &cb)#
Calls the provided callback for each thread that has not been joined/deleted.
This function provides a generalized subset of information that a TCB might contain to make it easier to introspect system state. Depending on the RTOS and its configuration, some of these fields may not be populated, so it is important to check that they have values before attempting to access them.
Warning: The function may disable the scheduler to perform a runtime capture of thread information.
Thread Snapshot Service#
pw_thread
offers an optional RPC service library
(:thread_snapshot_service
) that enables thread info capture of
running threads on a device at runtime via RPC. The service will guide
optimization of stack usage by providing an overview of thread information,
including thread name, stack bounds, and peak stack usage.
ThreadSnapshotService
currently supports peak stack usage capture for
all running threads (ThreadSnapshotService::GetPeakStackUsage()
) as well as
for a specific thread, filtering by name
(ThreadSnapshotService::GetPeakStackUsage(name=b"/* thread name */")
).
Thread information capture relies on the thread iteration facade which will
momentarily halt your RTOS, collect information about running threads, and
return this information through the service.
RPC service setup#
To expose a ThreadSnapshotService
in your application, do the following:
Create an instance of
pw::thread::proto::ThreadSnapshotServiceBuffer
. This template takes the number of expected threads, and uses it to properly size buffers required for aThreadSnapshotService
. If no thread count argument is provided, this defaults toPW_THREAD_MAXIMUM_THREADS
.Register the service with your RPC server.
For example:
#include "pw_rpc/server.h"
#include "pw_thread/thread_snapshot_service.h"
// Note: You must customize the RPC server setup; see pw_rpc.
pw::rpc::Channel channels[] = {
pw::rpc::Channel::Create<1>(&uart_output),
};
Server server(channels);
// Thread snapshot service builder instance.
pw::thread::proto::ThreadSnapshotServiceBuffer</*num threads*/>
thread_snapshot_service;
void RegisterServices() {
server.RegisterService(thread_snapshot_service);
// Register other services here.
}
void main() {
// ... system initialization ...
RegisterServices();
// ... start your application ...
}
-
PW_THREAD_MAXIMUM_THREADS#
The max number of threads to use by default for thread snapshot service.
-
constexpr size_t RequiredServiceBufferSize(const size_t num_threads)#
Function provided through the service to calculate buffer sizing. If no argument
num_threads
is specified, the function will takenum_threads
to bePW_THREAD_MAXIMUM_THREADS
.
Attention
Some platforms may only support limited subsets of this service depending on RTOS configuration. Ensure that your RTOS is configured properly before using this service. Please see the thread iteration documentation for your backend for more detail on RTOS support.
pw_snapshot integration#
pw_thread
provides some light, optional integration with pw_snapshot through
helper functions for populating a pw::Thread
proto. Some of these
are directly integrated into the RTOS thread backends to simplify the thread
state capturing for snapshots.
SnapshotStack()#
The SnapshotStack()
helper captures stack metadata (stack pointer and
bounds) into a pw::Thread
proto. After the stack bounds are
captured, execution is passed off to the thread stack collection callback to
capture a backtrace or stack dump. Note that this function does NOT capture the
thread name: that metadata is only required in cases where a stack overflow or
underflow is detected.
Python processor#
Threads captured as a Thread proto message can be dumped or further analyzed
using using pw_thread
’s Python module. This is directly integrated into
pw_snapshot’s processor tool to automatically provide rich thread state dumps.
The ThreadSnapshotAnalyzer
class may also be used directly to identify the
currently running thread and produce symbolized thread dumps.
Warning
Snapshot integration is a work-in-progress and may see significant API changes.