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.

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