0128: Abstracting Thread Creation#

Status: Open for Comments Intent Approved Last Call Accepted Rejected

Proposal Date: 2024-04-25

CL: pwrev/206670

Author: 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 vast majority 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:

Example#

// "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();
// "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 pw::thread::Options specific to the thread backend. For example, to create a FreeRTOS thread, one must instantiate a pw::thread::freertos::Options and configure it with a 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 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 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. boot.cc and 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 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 for details. The latest proposal exposes the thread name and stack size. Some alternatives have also been proposed (P3072).

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 for details.

CMSIS-RTOS#

The CMSIS-RTOS2 API 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 struct. It also provides functions for initializing and controlling the scheduler, such as osKernelStart.

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 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.

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.

OS / API

function

name

stack size

priority type

priority levels

C++ <thread>

std::thread

none

none

none

none

POSIX

pthread_create

C string

bytes

pthread_attr_setschedparam

at least 32

CMSIS-RTOS2 / Keil RTX5

osThreadNew

C string

bytes

osPriority_t

56

embOS

OS_TASK_Create()

C string
uses pointer

bytes

unsigned int

2³²-2

FreeRTOS

xTaskCreateStatic

C string

words

unsigned int

NuttX

(also POSIX APIs)

C string

bytes

int

256

ThreadX

tx_thread_create

C string

bytes

unsigned int (TX_MAX_PRIORITIES - 1)–0 (0 highest)

multiple of 32

pw::ThreadContext

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.

#include "pw_thread/thread.h"

void CreateThreads() {
  pw::Thread::Start([] { /* thread body */ ).detach();
}

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.

#include "pw_thread/thread.h"

pw::DefaultThreadContext context;

void CreateThreads() {
  pw::Thread(context, pw::ThreadAttrs(), [] { /* thread body */ }).detach();
}

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.

#include "pw_thread/thread.h"
#include "project/config.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:

// "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

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.

#include "pw_thread/thread.h"
#include "project/config.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:

// 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.

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.

// 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:

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:

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:

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:

// 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 pw::thread::Options.

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.

#include "pw_thread/attrs.h"
#include "my_project/config.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.

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.

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:

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:

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:

#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.

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.

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