Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
josuttis_nm_c20_the_complete_guide.pdf
Скачиваний:
44
Добавлен:
27.03.2023
Размер:
5.85 Mб
Скачать

12.2 Stop Sources and Stop Tokens

417

Without using syncOut(), the output might even have interleaved characters because the output from the main thread and the thread running task() might be completely mixed.

Stop Callbacks in Detail

The type of stop callback, stop_callback, is a class template with a very limited API. Effectively, it only provides a constructor to register a callable for a stop token and a destructor that unregisters the callable. Copying and moving are deleted and no other member function is provided.

The template parameter is the type of the callable and is usually deduced when the constructor is initialized:

auto func = [] { ... };

std::stop_callback cb{myToken, func}; // deduces stop_callback<decltype(func)>

The only public member besides constructor and destructor is callback_type, which is the type of the stored callable.

The constructors accept both lvalues (objects with a name) and rvalues (temporary objects or objects marked with std::move()):

auto func = [] { ... };

 

std::stop_callback cb1{myToken, func};

// copies func

std::stop_callback cb2{myToken, std::move(func)};

// moves func

std::stop_callback cb3{myToken, [] { ... }};

// moves the lambda

12.2.3 Constraints and Guarantees of Stop Tokens

The feature to deal with stop requests is pretty robust regarding a couple of scenarios that might occur in asynchronous contexts. However, it cannot avoid every pitfall.

The C++20 standard library guarantees the following:

All request_stop(), stop_requested(), and stop_possible() calls are synchronized.

The callback registration is guaranteed to be performed atomically. If there is a concurrent call to request_stop() from another thread, then either the current thread will see the request to stop and immediately invoke the callback on the current thread, or the other thread will see the callback registration and will invoke the callback before returning from request_stop().

The callable of a stop_callback is guaranteed not to be called after the destructor of the stop_callback returns.

The destructor of a callback waits for its callable to finish if it is just called by another thread (it does not wait for other callables to finish).

However, note the following constraints:

A callback should not throw. If an invocation of its callable exits via an exception, then std::terminate() is called.

Do not destroy a callback in its own callable. The destructor does not wait for the callback to finish.

418

Chapter 12: std::jthread and Stop Tokens

12.3std::jthread in Detail

Table Operations of objects of class jthread lists the API of std::thread. The column Diff notes member functions that Modified the behavior or are New compared to std::thread.

Operation

Effect

 

 

 

Diff

jthread t

Default constructor; creates a nonjoinable thread object

 

jthread t{f,. . . }

Creates an object representing a new thread that calls f (with ad-

 

 

ditional args) or throws std::system_error

 

 

jthread t{rv}

Move constructor; creates a new thread object, which gets the

 

 

state of rv, and makes rv nonjoinable

 

 

 

 

t.~jthread()

Destructor; calls request_stop() and join() if the object is

Mod

 

joinable

 

 

 

 

t = rv

Move assignment; move assigns the

state

of rv

to t (calls

Mod

 

request_stop() and join() if t is joinable)

 

 

t.joinable()

Yields true if t has an associated thread (is joinable)

 

t.join()

Waits for the associated thread to finish and makes the object non-

 

 

joinable (throws std::system_error if the thread is not join-

 

 

able)

 

 

 

 

t.detach()

Releases the association of t to

its

thread

while the

 

 

thread continues and makes the object nonjoinable (throws

 

 

std::system_error if the thread is not joinable)

 

 

t.request_stop()

Requests a stop on the associated stop token

 

 

New

t.get_stop_source()

Yields an object to request a stop from

 

 

 

New

t.get_stop_token()

Yields an object to check for a requested stop

 

 

New

t.get_id()

Returns a unique thread ID of the member type id if joinable or

 

 

a default constructed ID if not

 

 

 

 

t.native_handle()

Returns a platform-specific member type native_handle_type

 

 

for a non-portable handling of the thread

 

 

 

t1.swap(t2)

Swaps the states of t1 and t2

 

 

 

 

swap(t1, t2)

Swaps the states of t1 and t2

 

 

 

 

hardware_concurrency()

Static function with a hint about possible hardware threads

 

 

 

 

 

 

 

Table 12.4. Operations of objects of class jthread

Note that the member types id and native_handle_type are the same for both std::thread and std::jthread so it does not matter if you use decltype(mythread)::id or std::thread::id. That way, you can just replace std::thread in existing code with std::jthread without having to replace anything else.

12.3 std::jthread in Detail

419

12.3.1 Using Stop Tokens with std::jthread

Besides the fact that the destructor joins, the major benefit of std::jthread is that it automatically establishes the mechanism to signal a stop. For that, the constructor starting a thread creates a stop source, stores it as a member of the thread object, and passes the corresponding stop token to the called function in case that function takes an additional stop_token as first parameter.

You can also get the stop source and stop token via member functions from the thread:

std::jthread t1{[] (std::stop_token st) {

...

}};

...

 

foo(t1.get_token());

// pass stop token to foo()

...

 

std::stop_source ssrc{t1.get_stop_source()};

ssrc.request_stop();

// request stop on stop token of t1

Because get_token() and get_stop_source() return by value, both the stop source and the stop token can even be used after a thread has been detached and runs in the background.

Using Stop Tokens with a Collection of std::jthreads

If you start multiple jthreads, each thread has its own stop token. Note that this might result in a situation that stopping all threads might take longer than expected. Consider the following code:

{

std::vector<std::jthread> threads; for (int i = 0; i < numThreads; ++i) {

pool.push_back(std::jthread{[&] (std::stop_token st) {

while (!st.stop_requested()) {

...

}

}});

}

...

} // destructor stops all threads

At the end of the loop, the destructor stops all running threads similarly to the following code:

for (auto& t : threads) { t.request_stop(); t.join();

}

This means that we always wait for one thread to end before we signal stop to the next thread.

420

Chapter 12: std::jthread and Stop Tokens

Code like this can be improved by requesting a stop for all threads before calling join() for all of them (via the destructor):

{

std::vector<std::jthread> threads; for (int i = 0; i < numThreads; ++i) {

pool.push_back(std::jthread{[&] (std::stop_token st) { while (!st.stop_requested()) {

...

}

}});

}

...

// BETTER: request stops for all threads before we start to join them: for (auto& t : threads) {

t.request_stop();

}

} // destructor stops all threads

Now we first request a stop for all threads and threads might end before the destructor calls join() for the threads to finish. An example of this technique is demonstrated by the destructor of a coroutine thread pool.

Using the Same Stop Token for Multiple std::jthreads

It might also be necessary to request a stop for multiple threads using the same stop token. This can be done easily. Just create the stop token yourself or take the stop token from the first thread that has been started and start the (other) threads with this stop token as first argument. For example:

// initialize a common stop token for all threads: std::stop_source allStopSource;

std::stop_token allStopToken{allStopSource.get_token()};

for (int i = 0; i < 9; ++i) { threads.push_back(std::jthread{[] (std::stop_token st) {

...

while (!st.stop_requested()) {

...

}

},

allStopToken // pass token to this thread

});

}

Remember that the callable usually just takes all arguments passed. The internal stop token of the thread started is used only if Only if there is an additional stop token parameter for which no argument is passed.

See lib/atomicref.cpp for a complete example.