Doc. no.: P3072R0
Date: 2023-12-17
Audience: LEWG
Reply-to: Zhihao Yuan <zy@miator.net>

Hassle-free thread attributes

Introduction

P2019R4[1] proposes to allow specifying thread attributes, such as name and stack size, when constructing std::thread and std::jthread. In such an approach, users express the attributes as objects, one type per attribute. P3022R0[2], with a mind to standardize the existing practices, groups all standard attributes into one type. In both papers, the types to represent attributes are implementation-defined. This paper proposes a fully specified aggregate to represent all the thread attributes defined by the standard. The vendors can have extra types to carry more or different attributes, as this paper ensures that the different types require differently looking codes.

Here is a comparison of the major use cases of the three papers:

P2019

std::jthread thr(std::thread_name("worker"),
                 std::thread_stack_size_hint(16384),
                 [] { std::puts("standard"); });
std::jthread thr(__gnu_cxx::posix_schedpolicy(SCHED_FIFO),
                 [] { std::puts("vendor extension"); });

P3022

std::jthread::attributes attrs;
attrs.set_name("worker");
attrs.set_stack_size_hint(16384);

std::jthread thr(attrs, [] { std::puts("standard"); });
__gnu_cxx::posix_thread_attributes attrs;
attrs.set_schedpolicy(SCHED_FIFO);

std::jthread thr(attrs, [] { std::puts("vendor extension"); });

P3072

std::jthread thr({.name = "worker", .stack_size_hint = 16384},
                 [] { std::puts("standard"); });
std::jthread thr(__gnu_cxx::posix_thread_attributes{
                     .schedpolicy = SCHED_FIFO,
                 },
                 [] { std::puts("vendor extension"); });

Motivation

P2019R4[1:1] has provided excellent motivation for standardizing thread attributes. The rest of this section will focus on the additional motivation for specifying these attributes in an aggregate.

Enjoy familiar, natrual, and terse syntax
Attributes are declarative data to most of the programmers. “No two attributes can be of the same type” is an unheard-of restriction, and “calling a subroutine to specify an attribute” is a ubiquitous complication. Designated initializers are already familiar to the C++ users. They precisely specify the attributes, are naturally isolated from the other arguments by the surrounding braces, and are as terse as possible.
Be trivially ABI stable
Changes that can break an aggregate’s ABI are apparent and often break API simultaneously; therefore, we won’t make them. We never argue whether std::from_chars_result is ABI stable. The standard thread attribute aggregate, i.e., std::thread::attributes in this paper, is meant to be as stable as std::from_chars_result.

Discussion

The proposed content is compact enough to fit in here for further discussion. First, add a new inner class attributes in the scope of std::thread.

class thread
{
  public:
    struct attributes
    {
        std::string const &name = {};
        std::size_t stack_size_hint = 0;
    };

And then, add extra constructors to std::thread and std::jthread, one for each.

class thread
{template<class Attrs = attributes, class F, class... Args>
        requires std::is_invocable_v<F, Args...>
    explicit thread(Attrs, F &&, Args &&...);};

class jthread
{template<class Attrs = thread::attributes, class F, class... Args>
        requires std::is_invocable_v<F, Args...>
    explicit jthread(Attrs, F &&, Args &&...);};

They enable a variety of uses.

Specifying all standard thread attributes
std::thread t1({.name = std::format("worker {}", i), .stack_size_hint = 16384},
               [] { std::puts("everything"); });
As designators, neither attribute can repeat in the same list; .name must precede .stack_size_hint if both appear.
Specifying only the thread name
std::thread t2({.name = "gui"}, [] { std::puts("only name"); });
stack_size_hint has no effect if it compares equal to 0.
Providing only a hint to the stack size
std::thread t3({.stack_size_hint = 4096}, std::puts, "only size");
name has no effect if it is an empty string.
Declaring the attributes object as a variable
std::thread::attributes attrs{.name = std::format("worker {}", i)};
std::thread t4(attrs, std::puts, "lifetime extension");
Member of reference type extends the lifetime of its initializer in a braced aggregate initialization. Replacing the braces with parenthesizes loses lifetime extension, but designated initializers cannot appear inside parenthesizes in the first place.
Substituting in non-standard thread attributes
std::thread t5(__gnu_cxx::posix_thread_attributes{.schedpolicy = SCHED_FIFO},
               std::puts, "vendor extension");
The users cannot omit the type names for the non-standard attributes in front of the braced-init-list.

std::jthread offers the same capability.

Technical Decisions

Make thread::attributes platform-independent

In a survey from P3022[2:1], Boost.Thread stores OS-provided thread attribute handle in boost::thread::attributes on certain platforms. More specifically, pthread_attr_t on POSIX. This may give the audiences a misconception – the thread name may be managed by an opaque type so that a standard library implementation doesn’t need to allocate anything extra for that string. This is not true. A table in P2019 shows that no platform supports an attribute handle of that design. And Boost.Thread only supports setting the stack size, which is the least motivating attribute to be represented platform-dependently.

Since thread names often come with a 15-character limit on non-Windows platforms, do we want different guts when implementing the name attribute on different platforms, then?

#if defined(_WIN32)
    unique_ptr<char[]> name_;
#else
    char name_[16];
#endif

It turns out that std::string offers this, and only better. Therefore, using std::string in some form is entirely acceptable when specifying the thread name.

The motivation for making standard thread attribute types implementation-defined is weak. A thread::attributes that reuses existing standard library types brings less hassle when working with. Consequently, thread::attributes should be platform-independent in a given standard library implementation.

Declare the name attribute as string const&

In a prior discussion of P2019, LEWG concluded that thread attributes may depend on runtime values in many cases, such as thread names formatted with a counter. In today’s C++ standard, the best practice for creating strings of that kind is to use std::format. Therefore, the full solution to thread attributes must be optimal when combined with std::format.

If the name member of std::thread::attributes is of type char const*, one must call .c_str() or an equivalent member function on the result of std::format. The code to initialize is not only bumpy but also invites dangling pointers:

std::thread::attributes attrs{.name = std::format("worker {}", i).c_str()}; // dangling

If name is declared string_view, the dangling error hides even better:

std::thread::attributes attrs{.name = std::format("worker {}", i)};    // also dangling

Moreover, as a survey from P2019 suggested, all platforms expect thread names to be null-terminated. string_view does not guarantee its content to be null-terminated and requires extra work in the thread and jthread constructors.

If name is declared string, the last code snippet won’t be dangling. However, the size of the attributes struct will be doubled. The type won’t be trivial, will require more work to be moved or copied, and can easily trigger a copy:

auto launch_first(std::vector<std::string> const& names)
{
    return std::thread({.name = names.front()}, this);
}

Declaring name as string const& has none of the aforementioned issues.

Default a template parameter to thread::attributes

Assigning the default argument of a template parameter T to an aggregate enables braced initialization of an argument for a function parameter of type T without filling out the type name of the aggregate at the caller site. Declaring that function parameter as the aggregate has the same effect. So, can we allow vendor extensions by building an overload set?

P3072

template<class Attrs = attributes, class F, class... Args>
    requires is_invocable_v<F, Args...>
explicit thread(Attrs, F &&, Args &&...);

Alt.

template<class F, class... Args>
explicit thread(attributes, F &&, Args &&...);

template<class F, class... Args>
explicit thread(__extended_thread_attributes, F &&, Args &&...);

Unsurprisingly, there is a critical difference. If __extended_thread_attributes is declared like this,

struct __extended_thread_attributes
{
    char name[16];
    size_t stack = 0;
    int schedpolicy = SCHED_OTHER;
};

the following code will become ill-formed with the alternative design because the call is ambiguous.

std::thread t2({.name = "gui"}, [] { std::puts("only name"); });

If we were to pursue this design, name conflicts between the standard thread attributes, vendor extended attributes, and future standard thread attributes would have to be resolved at the member level.

The proposed design avoids this type of ambiguity by construction. The declarion above initializes only std::thread::attributes.

Alias jthread::attributes to thread::attributes

Doing so won’t be the reason that prevents us from evolving jthread::attributes independently from thread::attributes because adding a member to a public aggregate defined in the standard is not an option to begin with. This also means if we do want the content of jthread::attributes and thread::attributes to diverge, the decision must be made now. But as time of writing, we see no motivation in such a direction.

Allowing a variable declared std::thread::attributes to be shared by both std::thread and std::jthread constructors looks entirely acceptable. This also avoids the headache of debating the effect of the following code:

std::thread::attributes attrs{.name = std::format("worker {}", i)};
std::jthread t(attrs, std::puts, "ignored, ill-formed, or vendor extension?");

Implementation

P2019R4[1:2] has discussed the implementability of platform-independent thread names and stack size hints.

Here is a playground to demonstrate the proposed interface of this paper: 4597WxKM8Compiler Explorer

Combining these should give us a complete implementation.

References


  1. P2019R4 Thread attributes.
    https://wg21.link/p2019r4 ↩︎ ↩︎ ↩︎

  2. P3022R0 A Boring Thread Attributes Interface.
    https://wg21.link/p3022r0 ↩︎ ↩︎