| Doc. no.: | P3072R0 |
| Date: | 2023-12-17 |
| Audience: | LEWG |
| Reply-to: | Zhihao Yuan <zy@miator.net> |
Hassle-free thread attributes
Introduction
P2019R4 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, 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 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
In a survey from P3022, 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()};
If name is declared string_view, the dangling error hides even better:
std::thread::attributes attrs{.name = std::format("worker {}", i)};
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 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: 4597WxKM8
Combining these should give us a complete implementation.
References
Hassle-free thread attributes
Introduction
P2019R4[1] proposes to allow specifying thread attributes, such as name and stack size, when constructing
std::threadandstd::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
P3022
P3072
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.
std::from_chars_resultis ABI stable. The standard thread attribute aggregate, i.e.,std::thread::attributesin this paper, is meant to be as stable asstd::from_chars_result.Discussion
The proposed content is compact enough to fit in here for further discussion. First, add a new inner class
attributesin the scope ofstd::thread.And then, add extra constructors to
std::threadandstd::jthread, one for each.They enable a variety of uses.
.namemust precede.stack_size_hintif both appear.stack_size_hinthas no effect if it compares equal to 0.namehas no effect if it is an empty string.std::jthreadoffers the same capability.Technical Decisions
Make
thread::attributesplatform-independentIn a survey from P3022[2:1], Boost.Thread stores OS-provided thread attribute handle in
boost::thread::attributeson certain platforms. More specifically,pthread_attr_ton 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. AndBoost.Threadonly 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?
It turns out that
std::stringoffers this, and only better. Therefore, usingstd::stringin some form is entirely acceptable when specifying the thread name.The motivation for making standard thread attribute types implementation-defined is weak. A
thread::attributesthat reuses existing standard library types brings less hassle when working with. Consequently,thread::attributesshould be platform-independent in a given standard library implementation.Declare the
nameattribute asstring 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 withstd::format.If the
namemember ofstd::thread::attributesis of typechar const*, one must call.c_str()or an equivalent member function on the result ofstd::format. The code to initialize is not only bumpy but also invites dangling pointers:If
nameis declaredstring_view, the dangling error hides even better:Moreover, as a survey from P2019 suggested, all platforms expect thread names to be null-terminated.
string_viewdoes not guarantee its content to be null-terminated and requires extra work in thethreadandjthreadconstructors.If
nameis declaredstring, the last code snippet won’t be dangling. However, the size of theattributesstruct will be doubled. The type won’t be trivial, will require more work to be moved or copied, and can easily trigger a copy:Declaring
nameasstring const&has none of the aforementioned issues.Default a template parameter to
thread::attributesAssigning the default argument of a template parameter
Tto an aggregate enables braced initialization of an argument for a function parameter of typeTwithout 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
Alt.
Unsurprisingly, there is a critical difference. If
__extended_thread_attributesis declared like this,the following code will become ill-formed with the alternative design because the call is ambiguous.
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::attributestothread::attributesDoing so won’t be the reason that prevents us from evolving
jthread::attributesindependently fromthread::attributesbecause 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 ofjthread::attributesandthread::attributesto 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::attributesto be shared by bothstd::threadandstd::jthreadconstructors looks entirely acceptable. This also avoids the headache of debating the effect of the following code: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: 4597WxKM8
Combining these should give us a complete implementation.
References
P2019R4 Thread attributes.
https://wg21.link/p2019r4 ↩︎ ↩︎ ↩︎
P3022R0 A Boring Thread Attributes Interface.
https://wg21.link/p3022r0 ↩︎ ↩︎