C++ CONCURRENCY SUPPORT LIBRARY

Dean Turpin

Fri Mar 31 18:56:04 UTC 2023

Foward

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


#include <latch>

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.

#include "gtest/gtest.h"
#include <latch>
#include <thread>

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

The go-to platform-independent thread API. It’s been a lot easier since std::thread was introduced in C++11.

#include "gtest/gtest.h"
#include <algorithm>
#include <ranges>
#include <thread>
#include <vector>

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>

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.

#include "gtest/gtest.h"
#include <barrier>
#include <thread>

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

#include "gtest/gtest.h"
#include <new>

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 <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 gives us a convenient
  // synchonrisation point
  consumer.join();
  EXPECT_TRUE(q.empty());
}

#include <semaphore>

Like std::mutex but you’re saying you have multiple instances of a resource available for concurrent access.

#include "gtest/gtest.h"
#include <atomic>
#include <chrono>
#include <semaphore>
#include <thread>

std::counting_sempaphore

TEST(semaphore, counting_sempaphore) {
  // 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 <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.

#include "gtest/gtest.h"
#include <future>

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

#include "gtest/gtest.h"
#include <mutex>

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

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.

#include "gtest/gtest.h"
#include <execution>
#include <numeric>

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

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

std::stop_token

Built-in method to make a jthread stop.

#include "gtest/gtest.h"
#include <semaphore>
#include <stop_token>
#include <thread>

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

Update a variable thread safely. Can be used with any built-in type, or in fact, anything that is “trivially constructible”.

#include "gtest/gtest.h"
#include <atomic>

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

Benchmark

To paraphrase Shakira, the benchmarks don’t lie.


make: Entering directory '/builds/germs-dev/concurrency-support-library/benchmark'
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 -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 -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 -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 -march=native -mtune=native
g++ -c main.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 -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 -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 -march=native -mtune=native
g++ -o app.o atomics.o cache.o containers.o execution.o main.o semaphore.o thread.o -lfmt -lgtest -lbenchmark -lpthread -ltbb
timeout 60 ./app.o
------------------------------------------------------------------------------
Benchmark                                    Time             CPU   Iterations
------------------------------------------------------------------------------
variable_increment_unguarded             0.000 ns        0.000 ns   1000000000
variable_increment_with_atomic            6.80 ns         6.76 ns    103574601
variable_increment_with_mutex             7.47 ns         7.45 ns     93641679
variable_increment_with_scoped_lock       7.63 ns         7.59 ns     92370639
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                   10081211 ns     10035936 ns           70
push_front_deque                         37204 ns        37005 ns        19051
push_front_list                         439109 ns       437697 ns         1619
push_front_forward_list                 416755 ns       414523 ns         1688
push_back_vector                         36996 ns        36812 ns        18933
push_back_deque                          36566 ns        36375 ns        19236
push_back_list                          449544 ns       446127 ns         1570
insert_set                             1833189 ns      1820222 ns          396
insert_unordered_set                   1330227 ns      1320961 ns          529
emplace_set                            1944212 ns      1930136 ns          350
emplace_unordered_set                  1338753 ns      1334104 ns          520
map_insert                             1309263 ns      1301948 ns          533
unordered_map_insert                   1315970 ns      1264552 ns          550
populate_vector                           9118 ns         9067 ns        77285
populate_array                           0.000 ns        0.000 ns   1000000000
populate_valarray                         9148 ns         9100 ns        77393
exec_seq                                 0.000 ns        0.000 ns   1000000000
exec_par                                  5239 ns         5109 ns       136245
exec_par_unseq                            7599 ns         7352 ns        94277
exec_unseq                               0.000 ns        0.000 ns   1000000000
exec_seq/real_time                       0.000 ns        0.000 ns   1000000000
exec_par/real_time                        5085 ns         5027 ns       137789
exec_par_unseq/real_time                  7306 ns         7261 ns        96441
exec_unseq/real_time                     0.000 ns        0.000 ns   1000000000
semaphore_acquire_release                  675 ns          671 ns      1040490
thread_async                             20501 ns        12365 ns        57609
thread_thread                            18639 ns        11313 ns        61523
thread_jthread                           18717 ns        11365 ns        60837
thread_async/real_time                   20476 ns        12312 ns        34502
thread_thread/real_time                  19743 ns        11838 ns        35804
thread_jthread/real_time                 19679 ns        11813 ns        35561
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 -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 -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 -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 -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 -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 -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 -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 -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 -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 -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 -march=native -mtune=native
g++ -o app.o atomic.o barrier.o condition_variable.o execution.o future.o latch.o mutex.o new.o semaphore.o stop_token.o thread.o -lgtest_main -lfmt -lgtest -lbenchmark -lpthread -ltbb
timeout 60 ./app.o
Running main() from ./googletest/src/gtest_main.cc
[==========] 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_sempaphore
[       OK ] semaphore.counting_sempaphore (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. (2 ms total)
[  PASSED  ] 18 tests.
make: Leaving directory '/builds/germs-dev/concurrency-support-library/test'

CI info

PRETTY_NAME="Ubuntu Lunar Lobster (development branch)"
NAME="Ubuntu"
VERSION_ID="23.04"
VERSION="23.04 (Lunar Lobster)"
VERSION_CODENAME=lunar
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=lunar
LOGO=ubuntu-logo

Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Address sizes:                   46 bits physical, 48 bits virtual
Byte Order:                      Little Endian
CPU(s):                          1
On-line CPU(s) list:             0
Vendor ID:                       GenuineIntel
BIOS Vendor ID:                  Google
Model name:                      Intel(R) Xeon(R) CPU @ 2.30GHz
BIOS Model name:                   CPU @ 2.0GHz
BIOS CPU family:                 1
CPU family:                      6
Model:                           63
Thread(s) per core:              1
Core(s) per socket:              1
Socket(s):                       1
Stepping:                        0
BogoMIPS:                        4599.99
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
Hypervisor vendor:               KVM
Virtualization type:             full
L1d cache:                       32 KiB (1 instance)
L1i cache:                       32 KiB (1 instance)
L2 cache:                        256 KiB (1 instance)
L3 cache:                        45 MiB (1 instance)
NUMA node(s):                    1
NUMA node0 CPU(s):               0
Vulnerability Itlb multihit:     Not affected
Vulnerability L1tf:              Mitigation; PTE Inversion
Vulnerability Mds:               Mitigation; Clear CPU buffers; SMT Host state unknown
Vulnerability Meltdown:          Mitigation; PTI
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 generic retpoline, IBPB conditional, IBRS_FW, STIBP disabled, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Not affected

[email protected] 
-------------------------------------------------- 
OS: Ubuntu Lunar Lobster (development branch) x86_64 
Kernel: 5.4.109+ 
Uptime: 3 mins 
Packages: 310 (dpkg) 
Shell: bash 5.2.15 
CPU: Intel Xeon (1) @ 2.299GHz 
Memory: 452MiB / 3685MiB 


               total        used        free      shared  buff/cache   available
Mem:           3.6Gi       676Mi       2.0Gi       896Ki       1.1Gi       2.9Gi
Swap:          2.0Gi          0B       2.0Gi