C++ CONCURRENCY SUPPORT LIBRARY
See my other projects: deanturp.in.
Forward
This is an exploration of the headers referenced in the cppreference.com concurrency support library documentation. The source is compiled, tested and benchmarked on each commit. Additionally, the C-style comments are converted to markdown prior to rendering this web page; this is purely to aid legibility: all documentation really is in the code itself. Just for kicks -- and to keep repeat visits fresh -- the chapters are shuffled nightly.
Each source file can be run standalone in Godbolt.
g++-12 file.cxx -std=c++23 -O1 -lgtest -lgtest_main
Further reading
- C++ High Performance: Master the art of optimizing the functioning of your C++ code, 2nd Edition -- Bjorn Andrist, Viktor Sehr, Ben Garney
- C++ Concurrency in Action, Second Edition -- Anthony Williams
#include <semaphore>
#include "gtest/gtest.h"
#include <atomic>
#include <chrono>
#include <semaphore>
#include <thread>
std::counting_semaphore
Like std::mutex
but you're saying you have multiple instances of a resource
available for concurrent access.
TEST(semaphore, counting_semaphore) {
// Initialise to 2 resources and make them both available
auto sem = std::counting_semaphore<2>{2};
auto i = std::atomic{0uz};
// Grab one before starting the threads
sem.acquire();
// Create threads, the first will pass straight through
std::jthread t1{[&] {
sem.acquire();
++i;
}};
// This thread will block
std::jthread t2{[&] {
sem.acquire();
++i;
}};
// Check only one thread has updated the value
std::this_thread::sleep_for(std::chrono::microseconds{1});
EXPECT_EQ(i, 1);
// Release the second thread
sem.release();
// Confirm it has changed afterwards
std::this_thread::sleep_for(std::chrono::microseconds{1});
EXPECT_EQ(i, 2);
}
std::binary_semaphore
A binary semaphore is just a specialisation of a counting semaphore. These are equivalent:
auto sem = std::binary_semaphore{1};
auto sem = std::counting_semaphore<1>{1};
Let's create one and make it available immediately.
TEST(semaphore, binary_semaphore) {
// Initialise semaphore to available
auto sem = std::binary_semaphore{1};
auto i = size_t{0};
// Grab the semaphore before starting the thread
sem.acquire();
// Create thread and try semaphore
std::jthread t{[&] {
sem.acquire();
++i;
}};
// Check the thread is blocked
std::this_thread::sleep_for(std::chrono::microseconds{1});
EXPECT_EQ(i, 0);
// Release the thread
sem.release();
// Confirm value has changed afterwards
std::this_thread::sleep_for(std::chrono::microseconds{1});
EXPECT_EQ(i, 1);
}
#include <execution>
#include "gtest/gtest.h"
#include <execution>
#include <numeric>
Many of the Standard Library algorithms can take an execution policy, which is quite an exciting way to parallelise existing code. But remember it offers no thread safety: you must still protect your data as you would for any other threads. You also need to link against the TBB library.
// Create a big (-ish) chunk of data to play with
static const std::vector<int> vec = [] {
std::vector<int> v(10000);
std::iota(begin(v), end(v), 0);
return v;
}();
Some algorithms also have an _if
version that takes predicate: e.g.,
std::replace
and std::replace_if
.
std::sort
std::copy
std::transform
std::accumulate
std::for_each
std::reduce
std::inclusive_scan
std::exclusive_scan
std::transform_reduce
std::remove
std::count
std::max_element
std::min_element
std::find
std::generate
std::execution::par
Let's test each policy, and confirm the sums are equal.
It must be noted, though, that you must not throw exceptions in these routines
(even for the sequential option) or else crash and burn (std::terminate
.)
TEST(thread, execution_policy) {
// Sequential -- bof
const auto s0 = std::reduce(std::execution::seq, begin(vec), cend(vec));
// Parallel -- simple threads only
const auto s1 = std::reduce(std::execution::par, cbegin(vec), cend(vec));
// Parallel -- throw the whole tool shed at it
const auto s2 =
std::reduce(std::execution::par_unseq, cbegin(vec), cend(vec));
// Parallel -- vectorisation only
const auto s3 = std::reduce(std::execution::unseq, cbegin(vec), cend(vec));
EXPECT_EQ(s0, s1);
EXPECT_EQ(s0, s2);
EXPECT_EQ(s0, s3);
}
#include <new>
#include "gtest/gtest.h"
#include <new>
You can query cache line size programmatically with these constants. Ensure data used together by a single thread are co-located; and conversely, avoid false sharing by keeping unrelated data apart.
std::hardware_destructive_interference_size
TEST(new, interference) {
struct {
alignas(std::hardware_destructive_interference_size) int x0;
alignas(std::hardware_destructive_interference_size) int x1;
alignas(std::hardware_destructive_interference_size) int x2;
} together;
struct {
alignas(std::hardware_constructive_interference_size) int x0;
alignas(std::hardware_constructive_interference_size) int x1;
alignas(std::hardware_constructive_interference_size) int x2;
} apart;
EXPECT_GE(&together.x1 - &together.x0, 16);
EXPECT_LT(&apart.x1 - &apart.x0, std::hardware_destructive_interference_size);
EXPECT_EQ(std::hardware_destructive_interference_size, 64);
EXPECT_EQ(std::hardware_constructive_interference_size, 64);
}
#include <latch>
#include "gtest/gtest.h"
#include <latch>
#include <thread>
A latch is a single-use synchronisation primitive; single-use meaning you have to reset it. Really you could just always use a barrier which doesn't need resetting.
std::latch
TEST(latch, latch) {
// Initialise latch with the number of threads
auto l = std::latch{2};
// Variable to test how many threads have been allowed through the latch
auto i = std::atomic{0uz};
// Thread function waits for the others then updates a variable atomically
const auto func = [&]() {
l.arrive_and_wait();
++i;
};
// Create our threads
auto t1 = std::thread{func};
auto t2 = std::thread{func};
// Remember to join
t1.join();
t2.join();
EXPECT_EQ(i, 2);
}
#include <atomic>
#include "gtest/gtest.h"
#include <atomic>
Update a variable thread safely. Can be used with any built-in type, or in fact, anything that is "trivially constructible".
std::atomic
TEST(atomic, atomic) {
auto i = std::atomic{0uz};
// This is thread safe
++i;
EXPECT_EQ(i, 1);
}
std::atomic_ref
Wrap a non-atomic thing in atomic love.
TEST(atomic, atomic_ref) {
auto i = 0uz;
auto ii = std::atomic_ref{i};
// This is also thread safe
++ii;
EXPECT_EQ(ii, i);
EXPECT_EQ(ii, 1);
}
#include <stop_token>
#include "gtest/gtest.h"
#include <semaphore>
#include <stop_token>
#include <thread>
std::stop_token
Built-in method to make a jthread
stop.
Instruct the thread to exit its processing loop. I've used a semaphore to make the calling thread wait for the processing thread to tidy up.
TEST(thread, stop_token_explicit) {
using namespace std::literals::chrono_literals;
// Update this variable when we leave the stop token loop
auto i = 0uz;
// Create a semaphore to interact with the processing thread
std::binary_semaphore sem{0};
// Create processing thread
std::jthread t{[&](std::stop_token stop_token) {
// Do some work until told otherwise
while (not stop_token.stop_requested())
std::this_thread::sleep_for(1us);
// Our work here is done, update variable
++i;
// Tell the calling thread to continue
sem.release();
}};
// Check variable hasn't been updated, the processing thread is in its main
// loop at this point
EXPECT_EQ(i, 0);
// Tell the processing thread to stop
t.request_stop();
// Wait for it to finish
sem.acquire();
// Check variable has been updated
EXPECT_EQ(i, 1);
}
std::jthread
also stops implicitly when the thread goes out of scope.
TEST(thread, stop_token_implicit) {
// Update this variable when we leave the stop token loop
auto i = 0uz;
// Create a semaphore to interact with the processing thread
std::binary_semaphore sem{0};
// Create thread, it stops when it goes out of scope
{
std::jthread t{[&](std::stop_token stop_token) {
// Do some work until told otherwise
while (not stop_token.stop_requested())
std::this_thread::sleep_for(std::chrono::microseconds{1});
// Our work here is done, update variable and poke the calling thread
++i;
sem.release();
}};
// Check variable hasn't been updated, processing thread is doing its thing
EXPECT_EQ(i, 0);
// Thread goes out of scope and calls stop implicitly
}
// Check variable has been updated when processing thread signals
sem.acquire();
EXPECT_EQ(i, 1);
}
#include <barrier>
#include "gtest/gtest.h"
#include <barrier>
#include <thread>
A barrier is a multi-use latch. It is released when enough threads are queued up. Unlike a regular latch it also gives you the option of calling a routine when the latch is released.
std::barrier
TEST(latch, barrier) {
// Function to call when barrier opens
auto finished = bool{false};
const auto we_are_done = [&]() { finished = true; };
// Initialise barrier with the number of threads
std::barrier b(2, we_are_done);
// Thread function
const auto func = [&]() { b.arrive_and_wait(); };
// Create our threads (note braces for scope)
{
std::jthread t1{func};
std::jthread t2{func};
}
EXPECT_TRUE(finished);
}
#include <thread>
#include "gtest/gtest.h"
#include <algorithm>
#include <ranges>
#include <thread>
#include <vector>
The go-to platform-independent thread API. It's been a lot easier since
std::thread
was introduced in C++11.
std::thread
You must call join()
on the thread after creating it, otherwise bad
things. Typically this is managed using a vector of threads.
TEST(thread, thread) {
auto threads = std::vector<std::thread>{};
// Create threads
for ([[maybe_unused]] const auto _ : std::ranges::iota_view{1, 10})
threads.emplace_back([] {});
// Catch threads
for (auto &t : threads)
if (t.joinable())
t.join();
}
std::jthread
A joining thread is the same as a regular thread but has an implicit join()
in
the destructor. You can still join a std::jthread
, of course, which can be a
convenient synchronisation point.
TEST(thread, jthread) {
{
std::jthread t{[] {}};
}
// join() is called when it goes out of scope
}
std::this_thread
Useful functions within a thread.
TEST(thread, functions) {
// Suggest reschedule of threads
std::this_thread::yield();
// Get the ID of the current thread, useful for tracking/logging
const auto id = std::this_thread::get_id();
EXPECT_NE(id, std::thread::id{});
}
#include <condition_variable>
#include "gtest/gtest.h"
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
std::condition_variable
Like a mutex but with an additional predicate.
TEST(condition_variable, notify_one_with_predicate) {
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
// Consumer thread waits for data to change
std::jthread consumer{[&]() {
std::unique_lock<std::mutex> lock(m);
// Wait until the data change
cv.wait(lock, [&] { return not q.empty(); });
// Empty the queue
while (not q.empty())
q.pop();
}};
// Producer thread updates data
std::jthread producer{[&]() {
std::unique_lock<std::mutex> lock(m);
q.push(0);
q.push(1);
q.push(2);
EXPECT_FALSE(q.empty());
// Notify the other thread that something has changed
cv.notify_one();
}};
// You don't have to join a thread but it offers a convenient synchonrisation
// point
consumer.join();
EXPECT_TRUE(q.empty());
}
#include <mutex>
#include "gtest/gtest.h"
#include <mutex>
A mutual exclusion lock is the starting point for all things concurrent, offering a standard way to protect access a resource; but there are multiple ways to unlock it.
namespace {
int value{};
std::mutex mux{};
}; // namespace
std::mutex
To safely -- i.e., predictably -- update a value concurrently we must first lock it. You can lock/unlock explicitly (below), but this can quickly go wrong if the unlock is skipped: e.g, by bad logic or exceptions.
TEST(mutex, mutex) {
mux.lock();
value = 1;
mux.unlock();
EXPECT_EQ(value, 1);
}
std::lock_guard
Missing an unlock may result in a deadlock, so the Standard Library offers a few ways to mitigate this risk and unlock automatically using the RAII paradigm. Multiple mutexes can be acquired safely using scoped locks.
TEST(mutex, lock_guards) {
// Basic locking of a single mutex.
{
std::lock_guard lock(mux);
++value;
EXPECT_EQ(value, 2);
}
// Deadlock safe locking of one or more mutexes
{
std::mutex mux2;
std::scoped_lock lock(mux, mux2);
++value;
EXPECT_EQ(value, 3);
}
}
std::call_once
This can be emulated with a static IIFE function, but "call once" does express intention more directly.
TEST(mutex, call_once) {
// It's a bit of a shame you need this flag
std::once_flag flag;
auto i = size_t{0};
std::call_once(flag, [&]() { ++i; });
EXPECT_EQ(i, 1);
std::call_once(flag, [&]() { ++i; });
EXPECT_EQ(i, 1);
std::call_once(flag, [&]() { ++i; });
EXPECT_EQ(i, 1);
}
#include <future>
#include "gtest/gtest.h"
#include <future>
std::async
is a powerful way to handle return values from multiple threads,
think of it like pushing a calculation into the background. It executes a
function asynchronously and returns a std::future
that will eventually hold
the result of that function call. Quite a nice way to reference the result of
calculation executed in another thread. Of course you must factor in the
overhead of actually creating the thread -- 20µs, say; but your mileage may
vary.
std::async
TEST(thread, async) {
// Calculate some things in the background
auto a = std::async(std::launch::async, [] { return 1; });
auto b = std::async(std::launch::async, [] { return 2; });
auto c = std::async(std::launch::async, [] { return 3; });
// Block until they're all satisfied
const auto sum = a.get() + b.get() + c.get();
EXPECT_EQ(sum, 6);
}
Benchmark
make: Entering directory '/builds/germs-dev/concurrency-support-library/benchmark'
curl -L turpin.cloud/main.cxx --output /tmp/main.cxx
g++ -c atomics.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c cache.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c containers.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c execution.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c semaphore.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c thread.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -o app.o /tmp/main.cxx atomics.o cache.o containers.o execution.o semaphore.o thread.o -lfmt -lgtest -lbenchmark -lpthread -ltbb -std=c++23
timeout 60 ./app.o --benchmark_filter=
------------------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------------------
variable_increment_unguarded 0.000 ns 0.000 ns 1000000000
variable_increment_with_atomic 2.25 ns 2.25 ns 309854087
variable_increment_with_mutex 6.89 ns 6.89 ns 101897441
variable_increment_with_scoped_lock 7.20 ns 7.20 ns 97450320
sum_stride1 0.000 ns 0.000 ns 1000000000
sum_stride2 0.000 ns 0.000 ns 1000000000
sum_stride3 0.000 ns 0.000 ns 1000000000
sum_stride4 0.000 ns 0.000 ns 1000000000
sum_stride15 0.000 ns 0.000 ns 1000000000
sum_stride16 0.000 ns 0.000 ns 1000000000
sum_stride31 0.000 ns 0.000 ns 1000000000
sum_stride32 0.000 ns 0.000 ns 1000000000
insert_front_vector 7350306 ns 7335680 ns 95
push_front_deque 16654 ns 16642 ns 42131
push_front_list 323073 ns 323029 ns 2198
push_front_forward_list 274294 ns 274061 ns 2539
push_back_vector 12474 ns 12473 ns 55838
push_back_deque 18327 ns 18327 ns 38183
push_back_list 317207 ns 316980 ns 2199
insert_set 1196614 ns 1195808 ns 583
insert_unordered_set 756975 ns 756356 ns 920
emplace_set 1305807 ns 1305671 ns 520
emplace_unordered_set 760895 ns 760381 ns 930
map_insert 857139 ns 856519 ns 788
unordered_map_insert 659414 ns 658926 ns 1068
populate_vector 11092 ns 11092 ns 63564
populate_array 0.000 ns 0.000 ns 1000000000
populate_valarray 6718 ns 6717 ns 105169
exec_seq 0.000 ns 0.000 ns 1000000000
exec_par 5315 ns 5311 ns 131755
exec_par_unseq 6789 ns 6783 ns 103232
exec_unseq 0.000 ns 0.000 ns 1000000000
exec_seq/real_time 0.000 ns 0.000 ns 1000000000
exec_par/real_time 5369 ns 5368 ns 132069
exec_par_unseq/real_time 6748 ns 6747 ns 102112
exec_unseq/real_time 0.000 ns 0.000 ns 1000000000
semaphore_acquire_release 152 ns 152 ns 4645192
thread_async 38354 ns 28870 ns 24402
thread_thread 36605 ns 27863 ns 25433
thread_jthread 37453 ns 28397 ns 24865
thread_async/real_time 38407 ns 29184 ns 18645
thread_thread/real_time 36891 ns 28032 ns 18752
thread_jthread/real_time 37065 ns 28274 ns 18830
[==========] Running 0 tests from 0 test suites.
[==========] 0 tests from 0 test suites ran. (0 ms total)
[ PASSED ] 0 tests.
make: Leaving directory '/builds/germs-dev/concurrency-support-library/benchmark'
Unit test
make: Entering directory '/builds/germs-dev/concurrency-support-library/test'
g++ -c atomic.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c barrier.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c condition_variable.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c execution.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c future.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c latch.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c mutex.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c new.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c semaphore.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c stop_token.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -c thread.cxx -std=c++23 -O2 --all-warnings --extra-warnings --pedantic-errors -Werror -Wconversion -Wuninitialized -Weffc++ -Wunused -Wunused-variable -Wunused-function -Wshadow -Wfloat-equal -Wdelete-non-virtual-dtor -g -pg -march=native -mtune=native
g++ -o app.o /tmp/main.cxx atomic.o barrier.o condition_variable.o execution.o future.o latch.o mutex.o new.o semaphore.o stop_token.o thread.o -lfmt -lgtest -lbenchmark -lpthread -ltbb -std=c++23
timeout 60 ./app.o --benchmark_filter=
[==========] Running 18 tests from 7 test suites.
[----------] Global test environment set-up.
[----------] 2 tests from atomic
[ RUN ] atomic.atomic
[ OK ] atomic.atomic (0 ms)
[ RUN ] atomic.atomic_ref
[ OK ] atomic.atomic_ref (0 ms)
[----------] 2 tests from atomic (0 ms total)
[----------] 2 tests from latch
[ RUN ] latch.barrier
[ OK ] latch.barrier (0 ms)
[ RUN ] latch.latch
[ OK ] latch.latch (0 ms)
[----------] 2 tests from latch (0 ms total)
[----------] 1 test from condition_variable
[ RUN ] condition_variable.notify_one_with_predicate
[ OK ] condition_variable.notify_one_with_predicate (0 ms)
[----------] 1 test from condition_variable (0 ms total)
[----------] 7 tests from thread
[ RUN ] thread.execution_policy
[ OK ] thread.execution_policy (0 ms)
[ RUN ] thread.async
[ OK ] thread.async (0 ms)
[ RUN ] thread.stop_token_explicit
[ OK ] thread.stop_token_explicit (0 ms)
[ RUN ] thread.stop_token_implicit
[ OK ] thread.stop_token_implicit (0 ms)
[ RUN ] thread.thread
[ OK ] thread.thread (0 ms)
[ RUN ] thread.jthread
[ OK ] thread.jthread (0 ms)
[ RUN ] thread.functions
[ OK ] thread.functions (0 ms)
[----------] 7 tests from thread (1 ms total)
[----------] 3 tests from mutex
[ RUN ] mutex.mutex
[ OK ] mutex.mutex (0 ms)
[ RUN ] mutex.lock_guards
[ OK ] mutex.lock_guards (0 ms)
[ RUN ] mutex.call_once
[ OK ] mutex.call_once (0 ms)
[----------] 3 tests from mutex (0 ms total)
[----------] 1 test from new
[ RUN ] new.interference
[ OK ] new.interference (0 ms)
[----------] 1 test from new (0 ms total)
[----------] 2 tests from semaphore
[ RUN ] semaphore.counting_semaphore
[ OK ] semaphore.counting_semaphore (0 ms)
[ RUN ] semaphore.binary_semaphore
[ OK ] semaphore.binary_semaphore (0 ms)
[----------] 2 tests from semaphore (0 ms total)
[----------] Global test environment tear-down
[==========] 18 tests from 7 test suites ran. (1 ms total)
[ PASSED ] 18 tests.
make: Leaving directory '/builds/germs-dev/concurrency-support-library/test'
CI info
PRETTY_NAME="Ubuntu Mantic Minotaur (development branch)"
NAME="Ubuntu"
VERSION_ID="23.10"
VERSION="23.10 (Mantic Minotaur)"
VERSION_CODENAME=mantic
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=mantic
LOGO=ubuntu-logo
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 48 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 2
On-line CPU(s) list: 0,1
Vendor ID: AuthenticAMD
BIOS Vendor ID: Google
Model name: AMD EPYC 7B12
BIOS Model name: CPU @ 2.0GHz
BIOS CPU family: 1
CPU family: 23
Model: 49
Thread(s) per core: 2
Core(s) per socket: 1
Socket(s): 1
Stepping: 0
BogoMIPS: 4499.99
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext ssbd ibrs ibpb stibp vmmcall fsgsbase tsc_adjust bmi1 avx2 smep bmi2 rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 clzero xsaveerptr arat npt nrip_save umip rdpid
Hypervisor vendor: KVM
Virtualization type: full
L1d cache: 32 KiB (1 instance)
L1i cache: 32 KiB (1 instance)
L2 cache: 512 KiB (1 instance)
L3 cache: 16 MiB (1 instance)
NUMA node(s): 1
NUMA node0 CPU(s): 0,1
Vulnerability Itlb multihit: Not affected
Vulnerability L1tf: Not affected
Vulnerability Mds: Not affected
Vulnerability Meltdown: Not affected
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp
Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2: Mitigation; Full AMD retpoline, IBPB conditional, IBRS_FW, STIBP conditional, RSB filling
Vulnerability Srbds: Not affected
Vulnerability Tsx async abort: Not affected
root@runner-j2nyww-s-project-44474142-concurrent-0
--------------------------------------------------
OS: Ubuntu Mantic Minotaur (development branch) x86_64
Kernel: 5.4.109+
Uptime: 2 mins
Packages: 314 (dpkg)
Shell: bash 5.2.15
CPU: AMD EPYC 7B12 (2) @ 2.249GHz
Memory: 472MiB / 7963MiB
total used free shared buff/cache available
Mem: 7.8Gi 730Mi 5.7Gi 908Ki 1.6Gi 7.1Gi
Swap: 2.0Gi 0B 2.0Gi