blob: e15ad26c6d0f2ea9db52b0d05e82e7224165421d [file] [log] [blame]
.. _seed-0128:
=================================
0128: Abstracting Thread Creation
=================================
.. seed::
:number: 128
:name: Abstracting Thread Creation
:status: Accepted
:proposal_date: 2024-04-25
:cl: 206670
:authors: Wyatt Hepler
:facilitator: Taylor Cramer
-------
Summary
-------
This SEED proposes supporting cross-platform thread creation with ``pw_thread``.
It introduces APIs for creating a thread without referring to the specific OS /
``pw_thread`` backend. This dramatically simplifies thread creation for the
:ref:`vast majority <seed-0128-thread-config-survey>` of production use cases.
It does so without sacrificing configurability or limiting users in any way.
Key new features
================
- ``pw::ThreadAttrs`` describes cross-platform thread attributes:
- Thread name.
- Stack size.
- ``pw::ThreadPriority`` to represent a thread's priority.
- ``pw::ThreadContext`` represents the resources required to run one thread.
- ``pw::Thread`` can be started from ``ThreadAttrs`` and ``ThreadContext``.
- Additions to the ``pw_thread`` facade to support the new functionality.
pw_thread API overview
======================
With these changes, the key pw_thread features are as follows:
.. topic:: Thread creation API
Key Types
- ``pw::Thread`` -- Thread handle. The thread might be unstarted, running, or
completed.
- ``pw::thread::Options`` -- Base class for platform-specific thread options.
priority.
- ``pw::ThreadAttrs`` -- Generic thread attributes: name, size, priority.
may include stack.
- ``pw::ThreadPriority`` -- Generic thread priority, with relative modifiers.
- ``pw::ThreadContext`` -- Generic thread resources. Depending on backend,
- ``pw::ThreadStack`` -- Optionally specify a thread stack separately from
the context.
Key methods
- ``pw::Thread`` -- Constructor, ``join()``.
- ``pw::ThreadAttrs`` -- ``set_name(name)``, ``set_priority(priority)``,
``set_stack_size_bytes(bytes)``.
- ``pw::ThreadPriority`` -- ``Low()``, ``Medium()``, ``High()``,
``NextHigher()``, ``NextLower()``, etc.
Example
=======
.. code-block:: c++
// "example_project/threads.h"
// Define thread attributes for the main thread.
constexpr pw::ThreadAttrs
kMainThread = pw::ThreadAttrs().set_name("app").set_priority(
pw::ThreadPriority::Medium()),
.set_stack_size_bytes(MY_PROJECT_MAIN_STACK_SIZE_BYTES);
// Define attributes for another thread, based on kMainThread.
constexpr pw::ThreadAttrs kLogThread =
pw::ThreadAttrs(kMainThread).set_name("logging").set_priority_next_lower();
.. code-block:: c++
// "example_project/main.cc"
#include "example_project/threads.h"
// Declare a thread context that can be used to start a thread.
pw::ThreadContext<MY_PROJECT_APP_STACK_SIZE_BYTES> app_thread_context;
// Declare thread contexts associated with specific ThreadAttrs.
pw::ThreadContext<kMainThread> main_thread_context;
pw::ThreadContext<kLogThread> log_thread_context;
// Thread handle for a non-detached thread.
pw::Thread app_thread;
void StartThreads() {
// Start the main and logging threads.
pw::Thread(main_thread_context, MainThreadBody).detach();
pw::Thread(log_thread_context, LoggingThreadBody).detach();
// Start an app thread that uses the app_thread_context. Since the stack size
// is not specified, the full stack provided by app_thread_context is used.
app_thread = pw::Thread(
app_thread_context, pw::ThreadAttrs().set_name("app 1"), AppThreadBody1);
}
void MainThreadBody() {
// Join the "app 1" thread and reuse the app_thread_context for a new thread.
app_thread.join();
app_thread = pw::Thread(
app_thread_context, pw::ThreadAttrs().set_name("app 2"), AppThreadBody2);
// ...
}
----------
Motivation
----------
Pigweed's ``pw_thread`` module does not support cross-platform thread creation.
Instead, threads must be created by instantiating a
:cpp:class:`pw::thread::Options` specific to the thread backend. For example, to
create a FreeRTOS thread, one must instantiate a
:cpp:class:`pw::thread::freertos::Options` and configure it with a
:cpp:class:`pw::thread::freertos::Context`
Cross-platform thread creation was intentionally avoided in the ``pw_thread``
API. It is not possible to specify thread attributes in a truly generic,
portable way. Every OS/RTOS exposes a different set of thread parameters, and
settings for one platform may behave completely differently or not exist on
another.
Cross-platform thread creation may not be possible to do perfectly, but avoiding
it has significant downsides.
- The current APIs optimize for control at the expense of usability. Thread
creation is complex.
- Developers always have to deal with the full complexity of thread creation,
even for simple cases or when just getting started.
- Users must learn a slightly different API for each RTOS. The full ``Thread``
API cannot be documented in one place.
- Cross-platform code that creates threads must call functions that return
``pw::thread::Options``. Each platform implements the functions as needed.
This requires exposing threads in the public API. Libraries such as
:ref:`module-pw_system` cannot add internal threads without breaking their
users.
- Code for creating ``pw::thread::Options`` must be duplicated for each
platform.
- Projects avoid writing cross-platform code and tests due to the complexity of
thread creation.
``pw_system`` and threads
=========================
Currently, running :ref:`module-pw_system` requires writing custom low-level
code that is aware of both ``pw_system`` and the RTOS it is running on
(see e.g. :cs:`boot.cc
<4d23123c37a33638b2f1ce611423e74d385623ff:targets/stm32f429i_disc1_stm32cube/boot.cc;l=133>`
and :cs:`target_hooks.cc
<4d23123c37a33638b2f1ce611423e74d385623ff:pw_system/zephyr_target_hooks.cc>`).
Enabling cross-platform thread creation would make it easier to use
``pw_system``. The code for running ``pw_system`` on any target would be the
same: a single function call in ``main``. The user would no longer have to
allocate stacks or create :cpp:class:`pw::thread::Options` for ``pw_system``
threads; this could be managed by ``pw_system`` itself and configured with
generic ``pw_system`` options if needed.
Cross-platform thread creation also makes it easier for ``pw_system`` users to
write their own code. Setting up a thread takes just two lines of code and no
interactions with RTOS-specific APIs. A ``pw_system`` application created this
way can run on any platform out of the box.
---------------------
Problem investigation
---------------------
Various cross-platform threading APIs exist today.
C++ Standard Library
====================
The C++ Standard Library currently provides a limited cross-platform thread
creation API in ``<thread>``. No thread attributes are exposed; threads are
created with platform defaults.
An effort is underway to standardize some thread attributes, giving users more
control over threads while maintaining portability. See `P2019 -- Thread
attributes
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2019r6.pdf>`_ for
details. The latest proposal exposes the thread name and stack size. Some
alternatives have also been proposed (`P3072
<https://open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3072r2.html>`_).
POSIX
=====
POSIX is a portable operating system API. The POSIX thread creation function
``pthread_create`` takes a pointer to a ``pthread_attr_t`` struct. This struct
may a support a wide variety thread options that are configured with functions
such as ``pthread_attr_setstacksize``, ``pthread_attr_setschedpolicy``, and
others. A thread's name can be set with ``pthread_setname_np``. See `man
pthreads <https://man7.org/linux/man-pages/man7/pthreads.7.html>`_ for details.
CMSIS-RTOS
==========
The `CMSIS-RTOS2 API
<https://www.keil.com/pack/doc/CMSIS/RTOS2/html/index.html>`_ provides a generic
RTOS interface intended for use with Arm Cortex devices. CMSIS-RTOS2 is
implemented by several operating systems, including FreeRTOS and Arm's own Keil
RTX5.
CMSIS-RTOS2 provides a comprehensive set of thread attributes in its
`osThreadAttr_t
<https://www.keil.com/pack/doc/CMSIS/RTOS2/html/group__CMSIS__RTOS__ThreadMgmt.html#structosThreadAttr__t>`_
struct. It also provides functions for initializing and controlling the
scheduler, such as `osKernelStart
<https://www.keil.com/pack/doc/CMSIS/RTOS2/html/group__CMSIS__RTOS__KernelCtrl.html#ga9ae2cc00f0d89d7b6a307bba942b5221>`_.
--------
Proposal
--------
The new cross-platform API does not replace the existing backend-specific thread
creation APIs. The new API supports most production use cases, but does not
expose the full capabilities and configuration of all supported RTOSes. It is
intended to be easy to adopt, while providing a frictionless pathway to the
current, fully configurable APIs if needed.
With this proposal, per-target thread creation is simply a matter of setting
variables differently for each target. This removes the need for duplicated code
for creating platform-specific thread contexts and ``pw::thread::Options``.
Generic thread attributes
=========================
This SEED introduces a limited set of cross-platform thread attributes. These
generic attributes map to a platform-specific :cpp:class:`pw::thread::Options`.
There are three thread attributes:
- Name
- Stack size
- Priority
Other attributes may be added in the future, such as dynamic or static
resource allocation.
Thread attributes are provided only as hints to the backend. Backends should
respect thread attributes, if possible, but may ignore or adapt them depending
on the OS's capabilities. Backends cannot fail to create thread because of how
thread attributes are set, but users may check the backend's capabilities, such
as whether thread priorities are supported, as needed.
Examples of acceptable adaptations to thread attributes.
- Ignore the thread name and stack size because the underlying API does not
support specifying them (e.g. C++'s ``<thread>``).
- Silently truncate a thread name because the underlying RTOS only supports
shorter names.
- Round up to the minimum required stack size from a smaller requested stack
size.
- Add a fixed amount to a requested stack size to account for RTOS overhead.
- Dynamically allocate the thread stack if it is above a certain size;
statically allocate it otherwise.
.. _seed-0128-thread-config-survey:
Why these thread attributes?
----------------------------
A survey of thread creation with Pigweed across a few large, production projects
found that 99% of their thread configurations can be exactly represented with
thread name, priority, stack size. The only exception was a single RTOS feature
used in a few threads in one project.
The proof is in the pudding: ``pw_thread`` users almost never need low-level,
RTOS-specific threading features. Abstracting these three thread attributes
dramatically simplifies thread creation, resulting in more portable,
easier-to-test code. In the rare cases when more control is needed, the existing
non-portable ``pw_thread`` API is ready to use.
OS / RTOS support for thread attributes
---------------------------------------
Most OS APIs support the proposed thread attributes.
.. list-table::
:header-rows: 1
* - OS / API
- function
- name
- stack size
- priority type
- priority levels
* - C++ ``<thread>``
- `std::thread <https://en.cppreference.com/w/cpp/thread/thread/thread>`_
- none
- none
- none
- none
* - POSIX
- `pthread_create
<https://man7.org/linux/man-pages/man3/pthread_create.3.html>`_
- `C string
<https://man7.org/linux/man-pages/man3/pthread_setname_np.3.html>`_
- `bytes
<https://man7.org/linux/man-pages/man3/pthread_attr_setstacksize.3.html>`_
- `pthread_attr_setschedparam <https://man7.org/linux/man-pages/man3/pthread_attr_setschedparam.3.html>`_
- `at least 32
<https://man7.org/linux/man-pages/man2/sched_get_priority_max.2.html>`_
* - `CMSIS-RTOS2 / Keil RTX5 <https://arm-software.github.io/CMSIS_6/latest/RTOS2/group__CMSIS__RTOS__ThreadMgmt.html>`_
- `osThreadNew <https://arm-software.github.io/CMSIS_6/latest/RTOS2/group__CMSIS__RTOS__ThreadMgmt.html#ga48d68b8666d99d28fa646ee1d2182b8f>`_
- `C string
<https://arm-software.github.io/CMSIS_6/latest/RTOS2/group__CMSIS__RTOS__ThreadMgmt.html#structosThreadAttr__t>`__
- bytes
- `osPriority_t
<https://arm-software.github.io/CMSIS_6/latest/RTOS2/group__CMSIS__RTOS__ThreadMgmt.html#gad4e3e0971b41f2d17584a8c6837342ec>`_
- 56
* - `embOS <https://www.segger.com/downloads/embos/UM01001>`_
- ``OS_TASK_Create()``
- | C string
| uses pointer
- bytes
- ``unsigned int``
- 2³²-2
* - `FreeRTOS <https://www.freertos.org>`_
- `xTaskCreateStatic <https://www.freertos.org/xTaskCreateStatic.html>`_
- | C string
| copies `configMAX_TASK_NAME_LEN <https://www.freertos.org/a00110.html#configMAX_TASK_NAME_LEN>`_
- words
- `unsigned int <https://www.freertos.org/RTOS-task-priority.html>`_
- | `configMAX_PRIORITIES <https://www.freertos.org/a00110.html#configMAX_PRIORITIES>`_
| `≤32 in some configs <https://www.freertos.org/a00110.html#configUSE_PORT_OPTIMISED_TASK_SELECTION>`_
* - `NuttX <https://nuttx.apache.org/docs/latest/index.html>`_
- | `task_create <https://nuttx.apache.org/docs/latest/reference/user/01_task_control.html#c.task_create>`_
| (also POSIX APIs)
- C string
- bytes
- ``int``
- `256 <https://github.com/apache/nuttx/blob/0ed714bba4280f98f35cb0df1f9d668099604f97/include/sys/types.h#L81>`_
* - `ThreadX <https://github.com/eclipse-threadx/rtos-docs>`_
- `tx_thread_create
<https://github.com/eclipse-threadx/rtos-docs/blob/80bd9fe9a33fa79257c75629be1b4438b84db7bc/rtos-docs/threadx/chapter4.md#tx_thread_create>`_
- `C string
<https://github.com/eclipse-threadx/rtos-docs/blob/80bd9fe9a33fa79257c75629be1b4438b84db7bc/rtos-docs/threadx/chapter4.md#example-54>`__
- bytes
- ``unsigned int`` (``TX_MAX_PRIORITIES - 1``)–0 (0 highest)
- `multiple of 32
<https://github.com/eclipse-threadx/threadx/blob/80bd9fe9a33fa79257c75629be1b4438b84db7bc/common/inc/tx_api.h#L2143>`_
* - ``pw::ThreadContext``
- :cpp:type:`pw::Thread`
- C string
- bytes
- custom class
- same as underying OS
Creating threads
================
The APIs proposed in this SEED streamline thread creation for common use cases,
while allowing for full configuration when necessary.
Generally, projects should start with the minimum complexity required and
increase the complexity only if more control is needed. Threads defined in
upstream Pigweed should start with some configurability to avoid friction in
downstream projects.
Dynamic threads: "just give me a thread"
----------------------------------------
For simple cases, Pigweed will offer a new static ``pw::Thread::Start``
function.
.. code-block:: c++
#include "pw_thread/thread.h"
void CreateThreads() {
pw::Thread::Start([] { /* thread body */ ).detach();
}
.. admonition:: When should I use ``pw::Thread::Start``?
- Experimenting
- Prototyping
Declare a default thread
------------------------
Create a thread with ``DefaultThreadContext`` and default attributes. The
``pw_thread`` backend starts a thread with a default name, stack size, and
priority.
.. code-block:: c++
#include "pw_thread/thread.h"
pw::DefaultThreadContext context;
void CreateThreads() {
pw::Thread(context, pw::ThreadAttrs(), [] { /* thread body */ }).detach();
}
.. admonition:: When should I use default thread contexts?
- Experimenting
- Prototyping
- Testing
- Getting started
Configurable thread attributes
------------------------------
Define a ``pw::ThreadAttrs`` and use it to create threads with
``pw::ThreadContext<>``. Attributes are configured as needed using the project's
configuration pattern.
.. code-block:: c++
#include "project/config.h"
#include "pw_thread/thread.h"
constexpr auto kMyThread = pw::ThreadAttrs()
.set_name("my thread")
.set_priority(MY_THREAD_PRIORITY)
.set_stack_size_bytes(kMyThreadStackSizeBytes);
pw::ThreadContext<kMyThread> my_thread_context;
pw::Thread other_thread;
pw::ThreadContext<kOtherThreadStackSizeBytes> other_thread_context;
void StartThreads() {
pw::Thread(my_thread_context, [] { /* thread body */ }).detach();
other_thread = pw::Thread(other_thread_context,
pw::ThreadAttrs().set_name("other"),
OtherThreadBody);
}
Example configuration header:
.. code-block:: c++
// "project/config.h"
// Configurable thread priority. Can be changed by defining
// MY_THREAD_PRIORITY in the build system.
#ifndef MY_THREAD_PRIORITY
#define MY_THREAD_PRIORITY pw::ThreadPriority::High()
#endif // MY_THREAD_PRIORITY
// Configuration may be based on the target platform.
#if BUILDING_FOR_PLATFORM_A
inline constexpr size_t kMyThreadStackSizeBytes = 2048;
inline constexpr size_t kOtherThreadStackSizeBytes = 1024;
#else
inline constexpr size_t kMyThreadStackSizeBytes = 1536;
inline constexpr size_t kOtherThreadStackSizeBytes = 512;
#endif // BUILDING_FOR_PLATFORM_A
.. admonition:: When should I use configurable thread attributes?
- Pigweed upstream development
- Production project development
Platform-specific thread creation
---------------------------------
In the rare case that platform-specific thread configuration is required,
provide a function that returns ``NativeOptions`` or ``const Options&`` and use
it to create a thread. The function may be a facade, so each target can
implement it differently. Projects may provide a default implementation of the
function that uses ``pw::ThreadAttrs``.
This approach is equivalent to the original non-portable ``pw_thread`` creation
pattern, optionally with a ``pw::ThreadAttrs``-based default implementation of
the function. This approach is only necessary for threads that specifically
require non-portable features. Other threads should continue to use
``pw::ThreadAttrs``.
.. code-block:: c++
#include "project/config.h"
#include "pw_thread/thread.h"
// This function returns a `pw::thread::Options` for creating a thread.
pw::thread::NativeOptions GetThreadOptions();
// Optionally, provide a default implementation of `GetThreadOptions()` that
// uses `pw::ThreadAttrs`.
#if !PROJECT_CFG_THREAD_CUSTOM_OPTIONS
pw::thread::NativeOptions GetThreadOptions() {
static constinit pw::ThreadContext<project::cfg::kThreadStackSizeHintBytes>
context;
return pw::thread::GetNativeOptions(
context, pw::ThreadAttrs().set_name("thread name"));
}
#endif // !PROJECT_CFG_THREAD_CUSTOM_OPTIONS
// Call `GetThreadOptions()` to create a thread.
void CreateThreads() {
pw::Thread(GetThreadOptions(), [] { /* thread body */ }).detach();
}
Example configuration header:
.. code-block:: c++
// project/config.h
// Set to 1 to implement `GetThreadOptions()` and provide fully custom
// `pw::thread::Options` for the platform.
#ifndef PROJECT_CFG_THREAD_CUSTOM_OPTIONS
#define PROJECT_CFG_THREAD_CUSTOM_OPTIONS 0
#endif // PROJECT_CFG_THREAD_CUSTOM_OPTIONS
// Stack size setting for the default thread options.
#ifndef PROJECT_CFG_THREAD_STACKS_SIZE_HINT
#define PROJECT_CFG_THREAD_STACKS_SIZE_HINT 2048
#endif // PROJECT_CFG_THREAD_STACKS_SIZE_HINT
namespace project::cfg {
inline constexpr size_t kThreadStackSizeHintBytes =
PROJECT_CFG_THREAD_STACKS_SIZE_HINT;
} // namespace project::cfg
This approach is not recommended as a starting point. It adds complexity that is
unlikely to be necessary. Most projects should start with configurable
``ThreadAttrs`` and add switch to platform-specific thread configuration only
for threads that need it.
.. admonition:: When should I use platform-specific thread creation?
- Pigweed upstream development, if a downstream user specifically requires
platform-specific thread features for a thread defined by Pigweed.
- Production project development that requires platform-specific thread
features.
C++ implementation details
==========================
Facade additions
-----------------
This proposal adds a few items to the ``pw_thread`` facade:
- Aliases for the native context types wrapped by ``pw::ThreadContext``.
- Information about the range of supported thread priorities used by
``pw::ThreadPriority``.
- Alias for the native ``pw::thread::Options`` type.
- Function that maps ``pw::ThreadContext`` and ``pw::ThreadAttrs`` to native
``pw::thread::Options``.
These features are used by ``pw_thread`` classes, not end users.
.. code-block:: c++
// pw_thread_backend/thread_native.h
namespace pw::thread::backend {
// Native, non-templated context (resources).
using NativeContext = /* implementation-defined */;
// Thread context with a stack size hint. Must derive from or be the same
// type as `NativeContext`. Must be default constructible.
template <size_t kStackSizeHintBytes>
using NativeContextWithStack = /* implementation-defined */;
// Stack size to use when unspecified; 0 for platforms that do not support
// defining the stack size.
inline constexpr size_t kDefaultStackSizeBytes = /* implementation-defined */;
// Define the range of thread priority values. These values may represent a
// subset of priorities supported by the OS. The `kHighestPriority` may be
// numerically higher or lower than `kLowestPriority`, depending on the OS.
// Backends that do not support priorities must set `kLowestPriority` and
// `kHighestPriority` to the same value, and should use `int` for
// `NativePriority`.
using NativePriority = /* implementation-defined */;
inline constexpr NativePriority kLowestPriority = /* implementation-defined */;
inline constexpr NativePriority kHighestPriority = /* implementation-defined */;
// Native options class derived from pw::thread::Options.
using NativeOptions = /* implementation-defined */;
// Converts cross-platform ThreadAttrs to NativeOptions. May be defined
// in ``pw_thread_backend/thread_inline.h`` or in a .cc file.
NativeOptions GetNativeOptions(NativeContext& context,
const ThreadAttrs& attributes);
} // namespace pw::thread::backend
``pw_thread_stl`` example implementation:
.. code-block:: c++
namespace pw::thread::backend {
using NativeContext = pw::thread::stl::Context;
// Ignore the stack size since it's not supported.
template <size_t>
using NativeContextWithStack = pw::thread::stl::Context;
inline constexpr size_t kDefaultStackSizeBytes = 0;
using NativePriority = int;
inline constexpr NativePriority kLowestPriority = 0;
inline constexpr NativePriority kHighestPriority = 0;
using NativeOptions = pw::thread::stl::Options;
inline NativeOptions GetNativeOptions(NativeContext&, const ThreadAttrs&) {
return pw::thread::stl::Options();
}
} // namespace pw::thread::backend
``pw_thread_freertos`` example implementation:
.. code-block:: c++
namespace pw::thread::backend {
using NativeContext = pw::thread::freertos::StaticContext;
// Convert bytes to words, rounding up.
template <size_t kStackSizeBytes>
using NativeContextWithStack = pw::thread::stl::StaticContextWithStack<
(kStackSizeBytes + sizeof(StackType_t) - 1) / sizeof(StackType_t)>;
inline constexpr size_t kDefaultStackSizeBytes =
pw::thread::freertos::config::kDefaultStackSizeWords;
using NativePriority = UBaseType_t;
inline constexpr NativePriority kLowestPriority = tskIDLE_PRIORITY;
inline constexpr NativePriority kHighestPriority = configMAX_PRIORITIES - 1;
using NativeOptions = pw::thread::freertos::Options;
inline NativeOptions GetNativeOptions(NativeContext& context,
const ThreadAttrs& attrs) {
return pw::thread::freertos::Options().set_static_context(context),
.set_name(attrs.name()).set_priority(attrs.priority().native())
}
} // namespace pw::thread::backend
``ThreadPriority``
------------------
Different OS APIs define priorities very differently. Some support a few
priority levels, others support the full range of a ``uint32_t``. For some, 0 is
the lowest priority and for others it is the highest. And changing the OS's
scheduling policy might changes how threads are scheduled without changing their
priorities.
``pw::ThreadPriority`` represents thread priority precisely but abstractly. It
supports the following:
- Represent the full range of priorities supported by the underlying OS.
- Set priorities in absolute terms that map to OS priority ranges in a
reasonable way.
- Set priorities relative to one another.
- Check that priorities are actually higher or lower than one another on a given
platform at compile time.
- Check if the backend supports thread priorities at all.
Many projects will be able to define a single priority set for all platforms.
The priorities may translate differently to each platforms, but this may not
matter. If a single set of priorities does not work for all platforms,
priorities can be configured per platform, like other attributes.
Here is a high-level overview of the class:
.. code-block:: c++
namespace pw {
class ThreadPriority {
public:
// True if the backend supports different priority levels.
static constexpr bool IsSupported();
// Named priorities. These priority levels span the backend's supported
// priority range.
//
// The optional `kPlus` template parameter returns a priority the specified
// number of levels higher than the named priority, but never exceeding the
// priority of the next named level, if supported by the backend.
static constexpr ThreadPriority VeryLow<unsigned kPlus = 0>();
static constexpr ThreadPriority Low<unsigned kPlus = 0>();
static constexpr ThreadPriority MediumLow<unsigned kPlus = 0>();
static constexpr ThreadPriority Medium<unsigned kPlus = 0>();
static constexpr ThreadPriority MediumHigh<unsigned kPlus = 0>();
static constexpr ThreadPriority High<unsigned kPlus = 0>();
static constexpr ThreadPriority VeryHigh<unsigned kPlus = 0>();
// Refers to the lowest or highest priority supported by the OS.
static constexpr ThreadPriority Lowest<unsigned kPlus = 0>();
static constexpr ThreadPriority Highest();
// Returns the ThreadPriority with next distinct higher or lower value. If
// the priority is already the highest/lowest, returns the same value.
constexpr ThreadPriority NextLower();
constexpr ThreadPriority NextHigher();
// Returns the ThreadPriority with next distinct higher or lower value.
// Asserts that the priority is not already the highest/lowest.
constexpr ThreadPriority NextLowerChecked();
constexpr ThreadPriority NextHigherChecked();
// ThreadPriority supports comparison. This makes it possible, for example,
// to static_assert that one priority is higher than another in the
// backend.
constexpr bool operator==(const ThreadPriority&);
// ...
// Access the native thread priority type. These functions may be helpful
// when ThreadPriority is configured separately for each platform.
using native_type = backend::NativeThreadPriority;
static constexpr FromNative(native_type native_priority);
native_type native() const;
};
} // namespace pw
Example uses:
.. code-block:: c++
// Named priorities are spread over the backend's supported priority range.
constexpr pw::ThreadPriority kThreadOne = ThreadPriority::Low();
constexpr pw::ThreadPriority kThreadTwo = ThreadPriority::Medium();
// Define a priority one higher than Medium, but never equal to or greater
// than the next named priority, MediumHigh, if possible in the given
// backend.
constexpr pw::ThreadPriority kThreadThree = ThreadPriority::Medium<1>();
// Set the priority exactly one backend priority level higher than
// kThreadThree, if supported by the backend.
constexpr pw::ThreadPriority kThreadFour = kThreadThree.NextHigher();
static_assert(!ThreadPriority::IsSupported() || kThreadThree < kThreadFour);
.. tip::
It is recommended that projects pick a starting priority level (e.g.
``ThreadPriority::Lowest().NextHigher()``) and define all priorities relative
to it.
Mapping OS priorities to named priorities
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If thread priorities are not supported, all named priorities are the same level.
If fewer than 7 levels are supported by the backend, some named levels map to
the same OS priority. For example, if there are only 3 priority levels
supported, then ``VeryLow == Low``, ``MediumLow == Medium == MediumHigh``, and
``High == VeryHigh``.
For backends that support 7 or more priority levels, each named priority level
is guaranteed to map to a unique OS priority.
``ThreadAttrs``
---------------
The ``ThreadAttrs`` class represents generic thread attributes. It is a
cross-platform version of :cpp:class:`pw::thread::Options`.
.. code-block:: c++
namespace pw {
// Generic thread attributes.
class ThreadAttrs {
public:
// Initializes ThreadAttrs to their backend-defined defaults.
constexpr ThreadAttrs();
// ThreadAttrs can be copied to share properties between threads.
constexpr ThreadAttrs(const ThreadAttrs&) = default;
constexpr ThreadAttrs& operator=(const ThreadAttrs&) = default;
// Name hint as a null-terminated string; never null.
constexpr const char* name() const;
constexpr ThreadAttrs& set_name(const char* name);
constexpr Priority priority() const;
constexpr ThreadAttrs& set_priority(Priority priority);
// Increment or decrement the priority to set task priorities relative to
// one another.
constexpr ThreadAttrs& set_priority_next_higher();
constexpr ThreadAttrs& set_priority_next_lower();
constexpr size_t stack_size_bytes() const;
constexpr ThreadAttrs& set_stack_size_bytes(size_t stack_size_bytes);
};
} // namespace pw
``ThreadAttrs`` may be defined at runtime or as ``constexpr`` constants.
Projects may find it helpful to define ``ThreadAttrs`` in a centralized
location.
.. code-block:: c++
#include "my_project/config.h"
#include "pw_thread/attrs.h"
namespace my_project {
// Global list of thread attributes.
inline constexpr auto kThreadOne =
pw::ThreadAttrs()
.set_name("thread one")
.set_stack_size_bytes(1024)
.set_priority(pw::ThreadPriority::Medium());
inline constexpr auto kThreadTwo =
pw::ThreadAttrs(kThreadOne).set_name("thread two");
inline constexpr auto kImportantThread =
pw::ThreadAttrs()
.set_name("important!")
.set_stack_size_bytes(IMPORTANT_THREAD_STACK_SIZE_BYTES)
.set_priority(IMPORTANT_THREAD_PRIORITY);
inline constexpr auto kLessImportantThread =
pw::ThreadAttrs()
.set_name("also important!")
.set_stack_size_bytes(IMPORTANT_THREAD_STACK_SIZE_BYTES)
.set_priority(kImportantThread.priority().NextLower());
static_assert(
!pw::ThreadPriority::IsSupported() ||
kImportantThread.priority() > kLessImportantThread.priority(),
"If the platform supports priorities, ImportantThread must be higher "
"priority than LessImportantThread");
} // namespace my_project
``ThreadContext``
-----------------
``pw::ThreadContext`` represents the resources required to run one thread.
This may include platform-specific handles, a statically allocated thread
control block (TCB), or the thread's stack. If platforms do not require manual
allocation for threads, ``pw::ThreadContext`` may be empty.
``ThreadContext`` is a generic wrapper around a backend-defined object. It
prevents unintentional access of backend-specific features on the native object.
``ThreadContext`` objects may be reused if their associated thread has been
joined.
``ThreadContext`` takes a few forms:
- ``ThreadContext<kStackSizeHintBytes>`` -- Context with internally allocated
thread stack.
- ``ThreadContext<kThreadAttrs>`` -- Context associated with a set of
``ThreadAttrs``. Uses internally or externally allocated stack based on the
``ThreadAttrs``.
- ``ThreadContext<>`` -- Context with a runtime-provided ``ThreadStack``.
.. code-block:: c++
namespace pw {
// Represents the resources required for one thread. May include OS data
// structures, the thread stack, or be empty, depending on the platform.
//
// ThreadContext may be reused or deleted if the associated thread is
// joined.
template <auto>
class ThreadContext;
// ThreadContext with integrated stack.
template <size_t kStackSizeHintBytes,
size_t kAlignmentBytes = alignof(std::max_align_t)>
class ThreadContext {
public:
constexpr ThreadContext() = default;
private:
backend::NativeContextWithStack<kStackSizeHintBytes, kAlignmentBytes>
native_context_;
};
// Alias for ThreadContext with the backend's default stack size.
using DefaultThreadContext = ThreadContext<backend::kDefaultStackSizeBytes>;
// Declares a ThreadContext that is associated with a specific set of thread
// attributes. Internally allocates the stack if the stack size hint is set.
// The ThreadContext may be reused if the associated thread is joined, but
// all threads use the same ThreadAttrs.
template <const ThreadAttrs& kAttributes>
class ThreadContext {
private:
ThreadContext<kAttributes.stack_size_bytes()> context_;
};
} // namespace pw
#include "pw_thread_backend/thread_inline.h"
``ThreadStack``
---------------
Represents a thread stack of the specified size. The object may be empty if the
backends dynamically allocate stacks.
.. code-block:: c++
namespace pw {
template <size_t kStackSizeBytes>
class ThreadStack {
private:
backend::NativeThreadStack<kStackSizeBytes> native_stack_;
};
} // namespace pw
``ThreadStack`` may specified separately from the ``ThreadContext`` if users
have need to declare stacks in different sections or want to keep them separate
from other items in the ``ThreadContext``. The ``ThreadStack`` is set on the
``ThreadAttrs`` instead of the stack size:
.. code-block:: c++
STACK_SECTION alignas(256) constinit ThreadStack<kAppStackSizeBytes> kMainStack;
constexpr pw::ThreadAttrs kMainThread = pw::ThreadAttrs()
.set_name("MainThread")
.set_stack(kMainStack)
.set_priority(kMainPriority);
ThreadContext<kMainThread> kMainThreadContext;
void RunThread() {
pw::Thread(kMainThreadContext, [] { /* thread body */ }).detach();
}
``ThreadContext`` objects that are not associated with a ``ThreadAttrs`` work
similarly:
.. code-block:: c++
STACK_SECTION alignas(256) constinit ThreadStack<kAppStackSizeBytes> kAppStack;
ThreadContext<> kAppThreadContext;
void RunThreads() {
pw::Thread thread(kAppThreadContext,
pw::ThreadAttrs().set_stack(kAppStack).set_name("T1"),
[] { /* thread body */ });
thread.join()
pw::Thread thread(kAppThreadContext,
pw::ThreadAttrs().set_stack(kAppStack).set_name("T2"),
[] { /* thread body */ });
thread.join();
}
The ``STACK_SECTION`` macro would be provided by a config header:
.. code-block:: c++
#if BUILDING_FOR_DEVICE_A
#define STACK_SECTION PW_PLACE_IN_SECTION(".thread_stacks")
#else // building for device B
#define STACK_SECTION // section doesn't matter
#endif // BUILDING_FOR_DEVICE_A
``Thread`` additions
--------------------
``pw::Thread`` will accept ``ThreadContext`` and ``ThreadAttrs``.
.. code-block:: c++
class Thread {
// Existing constructor.
Thread(const Options& options, Function<void()>&& entry)
// Creates a thread with a ThreadContext associated with a ThreadAttrs.
template <const ThreadAttrs& kAttributes>
Thread(ThreadContext<kAttributes>& context, Function<void()>&& entry);
// Creates a thread from attributes passed in a template parameter.
template <const ThreadAttrs& kAttributes, size_t kStackSizeHintBytes>
Thread(ThreadContext<kStackSizeHintBytes>& context, Function<void()>&& entry);
// Creates a thread from context and attributes. Performs a runtime check
// that the ThreadContext's stack is large enough, which can be avoided by
// using one of the other constructors.
template <size_t kStackSizeHintBytes>
Thread(ThreadContext<kStackSizeHintBytes>& context,
const ThreadAttrs& attributes,
Function<void()>&& entry);
// Creates a thread with the provided context and attributes. The
// attributes have a ThreadStack set.
Thread(ThreadContext<>& context,
const ThreadAttrs& attributes,
Function<void()>&& entry);
Dynamic thread creation function
--------------------------------
The ``pw::Thread::Start`` function starts a thread as simply as possible. It
starts returns a ``pw::Thread`` that runs a user-provided function. Users may
optionally provide ``pw::ThreadAttrs``.
``pw::Thread::Start`` is implemented with a new, separate facade. The backend
may statically or dynamically allocate resources. A default backend that
statically allocates resources for a fixed number of threads will be provided in
upstream Pigweed.
.. code-block:: c++
namespace pw {
class Thread {
// ...
// Starts running the thread_body in a separate thread. The thread is
// allocated and managed by the backend.
template <typename Function, typename... Args>
static Thread Start(Function&& thread_body, Args&&... args);
template <typename Function, typename... Args>
static Thread Start(const pw::ThreadAttrs& attributes,
Function&& thread_body,
Args&&... args);
};
} // namespace pw