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

13.3 Extensions for Atomic Types

447

13.3.3 Atomic Floating-Point Types

Both std::atomic<> and std::atomic_ref<> now provide full specializations for types float, double, and long double.

In contrast to the primary template for arbitrary trivially copyable types, they provide the additional atomic operations to add and subtract a value:3

fetch_add(), fetch_sub()

operator+=, operator-=

Thus, the following is possible now:

std::atomic<double> d{0};

...

d += 10.3;

13.3.4 Thread Synchronization with Atomic Types

All atomic types (std::atomic<>, std::atomic_ref<>, and std::atomic_flag) now provide a simple API to let threads block and wait for changes of their values caused by other threads.

Thus, for an atomic value: std::atomic<int> aVal{100};

or an atomic reference: int value = 100;

std::atomic_ref<int> aVal{value};

you can define that you want to wait until the referenced value has changed:

int lastValue = aVal.load();

aVal.wait(lastValue); // block unless/until value changed (and notified)

If the value of the referenced object does not match the passed argument, it returns immediately. Otherwise, it blocks until notify_one() or notify_all() has been called for the atomic value or reference:

--aVal; // atomically modify the (referenced) value aVal.notify_all(); // notify all threads waiting for a change

However, as for condition variables, wait() might end due to a spurious wake-up (so without a called notification). Therefore, you should always double check the value after the wait().

The code to wait for a specific atomic value might look as follows: while ((int val = aVal.load()) != expectedVal) {

aVal.wait(val);

// here, aVal may or may not have changed

}

Note that there is no guarantee that you will get all updates. Consider the following program:

3In contrast to specializations to integral types, which also provide atomic support to increment/decrement values and perform bit-wise modifications.

448

Chapter 13: Concurrency Features

lib/atomicwait.cpp

#include <iostream> #include <thread> #include <atomic>

using namespace std::literals;

int main()

{

std::atomic<int> aVal{0};

// reader:

std::jthread tRead{[&] {

int lastX = aVal.load(); while (lastX >= 0) {

aVal.wait(lastX);

std::cout << "=> x changed to " << lastX << std::endl; lastX = aVal.load();

}

std::cout << "READER DONE" << std::endl;

}};

// writer:

std::jthread tWrite{[&] {

for (int newVal : { 17, 34, 3, 42, -1}) { std::this_thread::sleep_for(5ns);

aVal = newVal; aVal.notify_all();

}

}};

} ...

The output might be:

=> x changed to 17 => x changed to 34 => x changed to 3 => x changed to 42 => x changed to -1 READER DONE

or:

=> x changed to 17 => x changed to 3 => x changed to -1 READER DONE

13.3 Extensions for Atomic Types

449

or just:

READER DONE

Note that the notification functions are const member functions.

Fair Ticketing with Atomic Notifications

One application of using using atomic wait() and notifications is to use them like mutexes. This often pays off because using mutexes might be significantly more expensive.

Here is a example where we use atomics to implement a fair processing of values in a queue (compare with the unfair version using semaphores). Although multiple threads might wait, only a limited number of them might run. And by using a ticketing system, we ensure that the elements in the queue are processed in

order:4

 

 

lib/atomicticket.cpp

 

 

#include <iostream>

 

 

#include <queue>

 

 

#include <chrono>

 

 

#include <thread>

 

 

#include <atomic>

 

 

#include <semaphore>

 

 

using namespace std::literals;

// for duration literals

int main()

 

 

{

 

 

char actChar = 'a';

// character value iterating endless from ’a’ to ’z’

std::mutex actCharMx;

// mutex to access actChar

// limit the availability of threads with a ticket system:

std::atomic<int> maxTicket{0};

// maximum requested ticket no

std::atomic<int> actTicket{0};

// current allowed ticket no

// create and start a pool of numThreads threads: constexpr int numThreads = 10; std::vector<std::jthread> threads;

for (int idx = 0; idx < numThreads; ++idx) { threads.push_back(std::jthread{[&, idx] (std::stop_token st) {

while (!st.stop_requested()) {

// get next character value: char val;

{

4The idea for this example is based on an example by Bryce Adelstein Lelbach in his talk The C++20 Synchronization Library at the CppCon 2029 (see http://youtu.be/Zcqwb3CWqs4?t=1810).

450

Chapter 13: Concurrency Features

std::lock_guard lg{actCharMx}; val = actChar++;

if (actChar > 'z') actChar = 'a';

}

//request a ticket to process it and wait until enabled: int myTicket{++maxTicket};

int act = actTicket.load(); while (act < myTicket) {

actTicket.wait(act); act = actTicket.load();

}

//print the character value 10 times:

for (int i = 0; i < 10; ++i) { std::cout.put(val).flush();

auto dur = 20ms * ((idx % 3) + 1);

std::this_thread::sleep_for(dur);

}

 

// done, so enable next ticket:

++actTicket;

 

actTicket.notify_all();

}

 

}});

 

}

 

// enable and disable threads in the thread pool:

 

auto adjust = [&, oldNum = 0] (int newNum) mutable {

 

actTicket += newNum - oldNum;

// enable/disable tickets

if (newNum > 0) actTicket.notify_all();

// wake up waiting threads

oldNum = newNum;

 

};

 

for (int num : {0, 3, 5, 2, 0, 1}) {

std::cout << "\n====== enable " << num << " threads" << std::endl; adjust(num);

std::this_thread::sleep_for(2s);

}

for (auto& t : threads) { // request all threads to stop (join done when leaving scope) t.request_stop();

} }