C++ CONCURRENCY SUPPORT LIBRARY

See my other projects: turpin.dev

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 <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 <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 <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 <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 <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.

It's worth mentioning that a std::future is only moveable, but the Standard Library provides a shared future; so you can start multiple threads and pass in a value you don't yet know.

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);
}

#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 <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.

  1. std::sort
  2. std::copy
  3. std::transform
  4. std::accumulate
  5. std::for_each
  6. std::reduce
  7. std::inclusive_scan
  8. std::exclusive_scan
  9. std::transform_reduce
  10. std::remove
  11. std::count
  12. std::max_element
  13. std::min_element
  14. std::find
  15. 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 <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 <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 <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 <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);
}

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.23 ns         2.23 ns    313794277
variable_increment_with_mutex             6.64 ns         6.63 ns    101372955
variable_increment_with_scoped_lock       7.00 ns         7.00 ns    101120842
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                    7264073 ns      7259821 ns           95
push_front_deque                         17372 ns        17362 ns        40634
push_front_list                         288873 ns       288653 ns         2448
push_front_forward_list                 291021 ns       290993 ns         2656
push_back_vector                         12695 ns        12694 ns        55941
push_back_deque                          16858 ns        16857 ns        41759
push_back_list                          287865 ns       287846 ns         2441
insert_set                             1175832 ns      1175762 ns          593
insert_unordered_set                    781481 ns       781391 ns          894
emplace_set                            1244897 ns      1244780 ns          565
emplace_unordered_set                   775067 ns       774965 ns          911
map_insert                              843710 ns       843664 ns          833
unordered_map_insert                    665997 ns       665967 ns         1041
populate_vector                           7040 ns         7039 ns       103072
populate_array                           0.000 ns        0.000 ns   1000000000
populate_valarray                         7024 ns         7020 ns       105394
exec_seq                                 0.000 ns        0.000 ns   1000000000
exec_par                                  5219 ns         5198 ns       138244
exec_par_unseq                            7033 ns         7005 ns        99479
exec_unseq                               0.000 ns        0.000 ns   1000000000
exec_seq/real_time                       0.000 ns        0.000 ns   1000000000
exec_par/real_time                        5345 ns         5333 ns       124071
exec_par_unseq/real_time                  6884 ns         6879 ns        93091
exec_unseq/real_time                     0.000 ns        0.000 ns   1000000000
semaphore_acquire_release                  151 ns          151 ns      4614499
thread_async                             39449 ns        30798 ns        22422
thread_thread                            37835 ns        29635 ns        24598
thread_jthread                           39410 ns        30699 ns        22929
thread_async/real_time                   40156 ns        31467 ns        17812
thread_thread/real_time                  39768 ns        31100 ns        17584
thread_jthread/real_time                 39087 ns        30622 ns        18357
[==========] 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 (8 ms)
[----------] 2 tests from latch (9 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. (11 ms total)
[  PASSED  ] 18 tests.
make: Leaving directory '/builds/germs-dev/concurrency-support-library/test'

CI info

PRETTY_NAME="Ubuntu Noble Numbat (development branch)"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 (Noble Numbat)"
VERSION_CODENAME=noble
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=noble
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-xxurkrix-project-44474142-concurrent-0 
-------------------------------------------------- 
OS: Ubuntu Noble Numbat (development branch) x86_64 
Kernel: 5.4.109+ 
Uptime: 2 mins 
Packages: 341 (dpkg) 
Shell: bash 5.2.21 
CPU: AMD EPYC 7B12 (2) @ 2.249GHz 
Memory: 473MiB / 7963MiB 


               total        used        free      shared  buff/cache   available
Mem:           7.8Gi       730Mi       6.1Gi       904Ki       1.2Gi       7.1Gi
Swap:          2.0Gi          0B       2.0Gi