From 9b44d944ba6c8cf0cd749f281577e4ba238116bc Mon Sep 17 00:00:00 2001 From: Chris Cotter Date: Tue, 3 Feb 2026 05:05:41 +0000 Subject: [PATCH 1/9] Fix race in MPSC algo Relacy helped identify a race in the existing MPSC algo. I am having a hard time exactly explaining what's going on, but in the newly added unit test (the non Relacy one), I am able to observe three different odd behaviors - a consumer consuming the same elemment in an finite loop, apparently due to the internal next pointers pointing in some sort of cycle - consumer returning &__nil_! - consumer never able to consume a produced value (node is lost) With the non-relacy unit test, in the existing algo, if I insert a random sleep of 0-10 microseconds in push_back after __back_ is exchanged, I can observe one of the above behaviors nearly every single time. The most common was the first behavior. The existing algo claims it came from Dmitry Vyukov's implementation, though one key difference is that the existing one uses an atomic pointer to a Node for the "nil" object, whereas Dmitry's stores an actual Node object embedded in the queue. I re-implemented the version in stdexec exactly as it appears on Dmitry's website (which I had to dig up on archive.org), and it passes newly added Relacy (exploring many thread interleavings) and non-Relacy unit tests. I originally tracked down a bug in timed_thread_scheduler.cpp, where sometimes `STDEXEC_ASSERT(op->command_ == command_type::command_type::stop);` failed. --- include/exec/timed_thread_scheduler.hpp | 3 + .../__detail/__intrusive_mpsc_queue.hpp | 74 ++-- test/CMakeLists.txt | 1 + test/rrd/Makefile | 2 +- test/rrd/intrusive_mpsc_queue.cpp | 350 ++++++++++++++++++ .../detail/test_intrusive_mpsc_queue.cpp | 85 +++++ 6 files changed, 479 insertions(+), 36 deletions(-) create mode 100644 test/rrd/intrusive_mpsc_queue.cpp create mode 100644 test/stdexec/detail/test_intrusive_mpsc_queue.cpp diff --git a/include/exec/timed_thread_scheduler.hpp b/include/exec/timed_thread_scheduler.hpp index 841147ee0..faf8e6098 100644 --- a/include/exec/timed_thread_scheduler.hpp +++ b/include/exec/timed_thread_scheduler.hpp @@ -43,6 +43,9 @@ namespace exec { stop }; + // Default ctor for __intrusive_mpsc_queue's internal stub node + constexpr timed_thread_operation_base() = default; + constexpr timed_thread_operation_base( void (*set_value)(timed_thread_operation_base*) noexcept, command_type command = command_type::schedule) noexcept diff --git a/include/stdexec/__detail/__intrusive_mpsc_queue.hpp b/include/stdexec/__detail/__intrusive_mpsc_queue.hpp index fb89f618c..e980e9b57 100644 --- a/include/stdexec/__detail/__intrusive_mpsc_queue.hpp +++ b/include/stdexec/__detail/__intrusive_mpsc_queue.hpp @@ -30,53 +30,57 @@ namespace STDEXEC { template class __intrusive_mpsc_queue; + // _Node must be default_initializable only for the queue to construct an + // internal "stub" node - only the _Next data element is accessed internally. template _Node::* _Next> + requires __std::default_initializable<_Node> class __intrusive_mpsc_queue<_Next> { - __std::atomic __back_{&__nil_}; - void* __front_{&__nil_}; - __std::atomic<_Node*> __nil_ = nullptr; - constexpr void push_back_nil() { - __nil_.store(nullptr, __std::memory_order_relaxed); - auto* __prev = static_cast<_Node*>(__back_.exchange(&__nil_, __std::memory_order_acq_rel)); - (__prev->*_Next).store(&__nil_, __std::memory_order_release); - } + __std::atomic __back_{&__stub_}; + __std::atomic __head_{&__stub_}; + _Node __stub_; public: + + __intrusive_mpsc_queue() { + (__stub_.*_Next).store(nullptr, __std::memory_order_release); + } + constexpr auto push_back(_Node* __new_node) noexcept -> bool { - (__new_node->*_Next).store(nullptr, __std::memory_order_relaxed); - void* __prev_back = __back_.exchange(__new_node, __std::memory_order_acq_rel); - bool __is_nil = __prev_back == static_cast(&__nil_); - if (__is_nil) { - __nil_.store(__new_node, __std::memory_order_release); - } else { - (static_cast<_Node*>(__prev_back)->*_Next).store(__new_node, __std::memory_order_release); - } - return __is_nil; + (__new_node->*_Next).store(nullptr, __std::memory_order_release); + _Node* __prev = static_cast<_Node*>( + __head_.exchange(static_cast(__new_node), __std::memory_order_acq_rel) + ); + bool was_stub = __prev == &__stub_; + (__prev->*_Next).store(static_cast(__new_node), __std::memory_order_release); + return was_stub; } constexpr auto pop_front() noexcept -> _Node* { - if (__front_ == static_cast(&__nil_)) { - _Node* __next = __nil_.load(__std::memory_order_acquire); - if (!__next) { + _Node* __back = static_cast<_Node*>(__back_.load(__std::memory_order_relaxed)); + _Node* __next = static_cast<_Node*>((__back->*_Next).load(__std::memory_order_acquire)); + if (__back == &__stub_) { + if (nullptr == __next) return nullptr; - } - __front_ = __next; + __back_.store(static_cast(__next), __std::memory_order_relaxed); + __back = __next; + __next = static_cast<_Node*>((__next->*_Next).load(__std::memory_order_acquire)); + } + if (__next) { + __back_.store(static_cast(__next), __std::memory_order_relaxed); + return __back; } - auto* __front = static_cast<_Node*>(__front_); - void* __next = (__front->*_Next).load(__std::memory_order_acquire); + _Node* __head = static_cast<_Node*>(__head_.load(__std::memory_order_relaxed)); + if (__back != __head) + return nullptr; + push_back(&__stub_); + __next = static_cast<_Node*>((__back->*_Next).load(__std::memory_order_acquire)); if (__next) { - __front_ = __next; - return __front; + __back_.store(static_cast(__next), __std::memory_order_relaxed); + return __back; } - STDEXEC_ASSERT(!__next); - push_back_nil(); - do { - __spin_loop_pause(); - __next = (__front->*_Next).load(__std::memory_order_acquire); - } while (!__next); - __front_ = __next; - return __front; + return nullptr; } }; -} // namespace STDEXEC \ No newline at end of file + +} // namespace STDEXEC diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a9194d69c..33e211362 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -62,6 +62,7 @@ set(stdexec_test_sources stdexec/algos/other/test_execute.cpp stdexec/detail/test_completion_signatures.cpp stdexec/detail/test_utility.cpp + stdexec/detail/test_intrusive_mpsc_queue.cpp stdexec/schedulers/test_task_scheduler.cpp stdexec/queries/test_env.cpp stdexec/queries/test_get_forward_progress_guarantee.cpp diff --git a/test/rrd/Makefile b/test/rrd/Makefile index d68321426..b077640a5 100644 --- a/test/rrd/Makefile +++ b/test/rrd/Makefile @@ -7,7 +7,7 @@ build_dir = build .SECONDARY: -test_programs = split async_scope sync_wait +test_programs = split async_scope sync_wait intrusive_mpsc_queue test_exe_files = $(foreach name,$(test_programs),$(build_dir)/$(name)) diff --git a/test/rrd/intrusive_mpsc_queue.cpp b/test/rrd/intrusive_mpsc_queue.cpp new file mode 100644 index 000000000..8fa3c4cc1 --- /dev/null +++ b/test/rrd/intrusive_mpsc_queue.cpp @@ -0,0 +1,350 @@ +#include "../../relacy/relacy_std.hpp" + +#include + +struct test_node { + std::atomic next_{nullptr}; + int value_{0}; + + test_node() = default; + explicit test_node(int val) : value_(val) {} +}; + +using test_queue = STDEXEC::__intrusive_mpsc_queue<&test_node::next_>; + +struct mpsc_single_producer_consumer : rl::test_suite { + test_queue queue; + test_node node1{42}; + test_node node2{100}; + int consumed_count{0}; + int values_sum{0}; + + void thread(unsigned thread_id) { + if (thread_id == 0) { + queue.push_back(&node1); + queue.push_back(&node2); + } else { + while (consumed_count < 2) { + test_node* node = queue.pop_front(); + if (node) { + values_sum += node->value_; + ++consumed_count; + } + } + } + } + + void after() { + RL_ASSERT(consumed_count == 2); + RL_ASSERT(values_sum == 142); // 42 + 100 + } +}; + +struct mpsc_two_producers : rl::test_suite { + test_queue queue; + test_node nodes[4] = {test_node{1}, test_node{2}, test_node{3}, test_node{4}}; + std::atomic consumed_count{0}; + std::atomic seen[4]; + + void before() { + consumed_count.store(0, std::memory_order_relaxed); + for (int i = 0; i < 4; ++i) { + seen[i].store(false, std::memory_order_relaxed); + } + } + + void thread(unsigned thread_id) { + if (thread_id == 0) { + // Producer 1 + queue.push_back(&nodes[0]); + queue.push_back(&nodes[1]); + } else if (thread_id == 1) { + // Producer 2 + queue.push_back(&nodes[2]); + queue.push_back(&nodes[3]); + } else { + // Consumer + int count = 0; + while (count < 4) { + test_node* node = queue.pop_front(); + if (node) { + int idx = node->value_ - 1; + RL_ASSERT(idx >= 0 && idx < 4); + bool was_seen = seen[idx].exchange(true, std::memory_order_relaxed); + RL_ASSERT(!was_seen); // Each node should be seen exactly once + ++count; + } + } + consumed_count.store(count, std::memory_order_relaxed); + } + } + + void after() { + RL_ASSERT(consumed_count.load() == 4); + for (int i = 0; i < 4; ++i) { + RL_ASSERT(seen[i].load()); + } + } +}; + +struct mpsc_push_return_value : rl::test_suite { + test_queue queue; + test_node node1{1}; + test_node node2{2}; + test_node node3{3}; + + void thread(unsigned thread_id) { + RL_ASSERT(queue.push_back(&node1)); + RL_ASSERT(!queue.push_back(&node2)); + RL_ASSERT(!queue.push_back(&node3)); + + queue.pop_front(); + RL_ASSERT(!queue.push_back(&node1)); + + queue.pop_front(); + queue.pop_front(); + queue.pop_front(); + + RL_ASSERT(queue.push_back(&node1)); + RL_ASSERT(!queue.push_back(&node2)); + RL_ASSERT(!queue.push_back(&node3)); + } +}; + +struct mpsc_fifo_order : rl::test_suite { + test_queue queue; + test_node nodes[3] = {test_node{1}, test_node{2}, test_node{3}}; + int order[3]; + int consumed_count{0}; + + void before() { + for (int i = 0; i < 3; ++i) { + order[i] = -1; + } + } + + void thread(unsigned thread_id) { + if (thread_id == 0) { + queue.push_back(&nodes[0]); + queue.push_back(&nodes[1]); + queue.push_back(&nodes[2]); + } else { + int pop_order = 0; + while (consumed_count < 3) { + test_node* node = queue.pop_front(); + if (node) { + int idx = node->value_ - 1; + order[idx] = pop_order++; + ++consumed_count; + } + } + } + } + + void after() { + RL_ASSERT(consumed_count == 3); + RL_ASSERT(order[0] == 0); + RL_ASSERT(order[1] == 1); + RL_ASSERT(order[2] == 2); + } +}; + +struct mpsc_pop_from_empty_never_returns_node : rl::test_suite { + test_queue queue; + test_node node{99}; + std::atomic pushed{false}; + + void thread(unsigned thread_id) { + if (thread_id == 0) { + queue.push_back(&node); + pushed.store(true); + } else { + while (!pushed.load()); + + test_node* node = queue.pop_front(); + RL_ASSERT(node->value_ == 99); + + for (int i = 0; i != 10; ++i) { + node = queue.pop_front(); + RL_ASSERT(node == nullptr); + } + } + } +}; + +struct mpsc_five_prod_one_cons : rl::test_suite { + test_queue queue; + test_node nodes[10] = { + // Producer 0: values 0-1 + test_node{0}, test_node{1}, + // Producer 1: values 10000-10001 + test_node{10000}, test_node{10001}, + // Producer 2: values 20000-20001 + test_node{20000}, test_node{20001}, + // Producer 3: values 30000-30001 + test_node{30000}, test_node{30001}, + // Producer 4: values 40000-40001 + test_node{40000}, test_node{40001} + }; + std::atomic consumed_count{0}; + std::atomic seen[10]; + + void before() { + consumed_count.store(0, std::memory_order_relaxed); + for (int i = 0; i < 10; ++i) { + seen[i].store(false, std::memory_order_relaxed); + } + } + + void thread(unsigned thread_id) { + if (thread_id < 5) { + // Producer threads (0-4) + int base_idx = thread_id * 2; + queue.push_back(&nodes[base_idx]); + queue.push_back(&nodes[base_idx + 1]); + } else { + // Consumer thread (5) + std::atomic count = 0; + while (count < 10) { + test_node* node = queue.pop_front(); + if (node) { + // Map value to index + int idx; + if (node->value_ < 10000) { + idx = node->value_; // 0 or 1 + } else { + int producer_id = node->value_ / 10000; + int item_in_producer = node->value_ % 10000; + idx = producer_id * 2 + item_in_producer; + } + RL_ASSERT(idx >= 0 && idx < 10); + bool was_seen = seen[idx].exchange(true, std::memory_order_relaxed); + RL_ASSERT(!was_seen); // Each node should be seen exactly once + count.fetch_add(1); + } + } + consumed_count.store(count, std::memory_order_relaxed); + } + } + + void after() { + RL_ASSERT(consumed_count.load() == 10); + for (int i = 0; i < 10; ++i) { + RL_ASSERT(seen[i].load()); + } + } +}; + +struct mpsc_five_producers_ordered : rl::test_suite { + static constexpr int ITEMS_PER_PRODUCER = 100; + static constexpr int NUM_PRODUCERS = 5; + static constexpr int TOTAL_ITEMS = ITEMS_PER_PRODUCER * NUM_PRODUCERS; + + test_queue queue; + test_node nodes[TOTAL_ITEMS]; + std::atomic consumed_count{0}; + int consumed_values[TOTAL_ITEMS]; + + void before() { + consumed_count.store(0, std::memory_order_relaxed); + for (int i = 0; i < TOTAL_ITEMS; ++i) { + consumed_values[i] = -1; + // Initialize nodes with their values + // Producer 0: 1-100, Producer 1: 101-200, etc. + nodes[i].value_ = i + 1; + } + } + + void thread(unsigned thread_id) { + if (thread_id < NUM_PRODUCERS) { + int start_idx = thread_id * ITEMS_PER_PRODUCER; + for (int i = 0; i < ITEMS_PER_PRODUCER; ++i) { + queue.push_back(&nodes[start_idx + i]); + } + } else { + int count = 0; + while (count < TOTAL_ITEMS) { + test_node* node = queue.pop_front(); + if (node) { + consumed_values[count] = node->value_; + ++count; + } + } + consumed_count.store(count, std::memory_order_relaxed); + } + } + + void after() { + RL_ASSERT(consumed_count.load() == TOTAL_ITEMS); + + // Check that each value appears exactly once + bool seen[TOTAL_ITEMS + 1] = {false}; // values are 1-500, so need 501 elements + for (int i = 0; i < TOTAL_ITEMS; ++i) { + int value = consumed_values[i]; + RL_ASSERT(value >= 1 && value <= TOTAL_ITEMS); + RL_ASSERT(!seen[value]); // Each value should appear exactly once + seen[value] = true; + } + + // Group consumed values into 5 arrays based on their range + int range_values[NUM_PRODUCERS][ITEMS_PER_PRODUCER]; + int range_counts[NUM_PRODUCERS] = {0}; + + for (int i = 0; i < TOTAL_ITEMS; ++i) { + int value = consumed_values[i]; + // Determine which producer this value belongs to (0-4) + int producer = (value - 1) / ITEMS_PER_PRODUCER; + RL_ASSERT(producer >= 0 && producer < NUM_PRODUCERS); + range_values[producer][range_counts[producer]++] = value; + } + + // Verify each producer contributed exactly ITEMS_PER_PRODUCER items + for (int producer = 0; producer < NUM_PRODUCERS; ++producer) { + RL_ASSERT(range_counts[producer] == ITEMS_PER_PRODUCER); + } + + // Verify each range is in ascending order + for (int producer = 0; producer < NUM_PRODUCERS; ++producer) { + int range_start = producer * ITEMS_PER_PRODUCER + 1; + for (int i = 0; i < ITEMS_PER_PRODUCER; ++i) { + RL_ASSERT(range_values[producer][i] == range_start + i); + } + } + } +}; + +auto main(int argc, char** argv) -> int { + int iterations = argc > 1 ? strtol(argv[1], nullptr, 10) : 500000; + rl::test_params p; + p.iteration_count = iterations; + p.execution_depth_limit = 10000; + p.search_type = rl::random_scheduler_type; + +#define CHECK(x) if (!(x)) { std::cout << "Test " #x " failed\n"; return 1; } + + printf("Running mpsc_single_producer_consumer...\n"); + CHECK(rl::simulate(p)); + + printf("Running mpsc_two_producers...\n"); + CHECK(rl::simulate(p)); + + printf("Running mpsc_push_return_value...\n"); + CHECK(rl::simulate(p)); + + printf("Running mpsc_fifo_order...\n"); + CHECK(rl::simulate(p)); + + printf("Running five_prod_one_cons...\n"); + CHECK(rl::simulate(p)); + + printf("Running mpsc_pop_from_empty_never_returns_node...\n"); + CHECK(rl::simulate(p)); + + // Beefy test... + p.iteration_count = 50000; + printf("Running mpsc_five_producers_ordered...\n"); + CHECK(rl::simulate(p)); + + printf("All tests passed!\n"); + return 0; +} diff --git a/test/stdexec/detail/test_intrusive_mpsc_queue.cpp b/test/stdexec/detail/test_intrusive_mpsc_queue.cpp new file mode 100644 index 000000000..1ab81824a --- /dev/null +++ b/test/stdexec/detail/test_intrusive_mpsc_queue.cpp @@ -0,0 +1,85 @@ +#include +#include + +#include +#include +#include +#include +#include + +namespace { + + struct test_node { + std::atomic next_{nullptr}; + int value_{0}; + + test_node() = default; + + explicit test_node(int val) + : value_(val) { + } + }; + + using test_queue = STDEXEC::__intrusive_mpsc_queue<&test_node::next_>; + + TEST_CASE( + "intrusive_mpsc_queue with 2 producers and 1 consumer", + "[detail][intrusive_mpsc_queue]") { + test_queue queue; + constexpr int num_items_per_producer = 500; + constexpr int num_producers = 2; + constexpr int total_items = num_items_per_producer * num_producers; + + std::vector> nodes1; + std::vector> nodes2; + + for (int i = 0; i < num_items_per_producer; ++i) { + nodes1.push_back(std::make_unique(i * 1000)); + nodes2.push_back(std::make_unique(i * 1000 + 1)); + } + + std::atomic produced_count{0}; + + std::set consumed_addrs; + + std::jthread producer1([&]() { + for (int i = 0; i < num_items_per_producer; ++i) { + queue.push_back(nodes1[i].get()); + produced_count.fetch_add(1, std::memory_order_relaxed); + } + }); + + std::jthread producer2([&]() { + for (int i = 0; i < num_items_per_producer; ++i) { + queue.push_back(nodes2[i].get()); + produced_count.fetch_add(1, std::memory_order_relaxed); + } + }); + + std::set consumed; + std::jthread consumer([&]() { + int count = 0; + while (count < total_items) { + test_node* node = queue.pop_front(); + if (node) { + consumed.insert(node->value_); + ++count; + } else { + std::this_thread::yield(); + } + } + }); + + producer1.join(); + producer2.join(); + consumer.join(); + + REQUIRE(consumed.size() == total_items); + + for (int i = 0; i < num_items_per_producer; ++i) { + CHECK(consumed.count(i * 1000) == 1); + CHECK(consumed.count(i * 1000 + 1) == 1); + } + } + +} // namespace From 5a875d430e73fd9f88ad6be70131805f6c22a41a Mon Sep 17 00:00:00 2001 From: Chris Cotter Date: Wed, 4 Feb 2026 14:40:20 +0000 Subject: [PATCH 2/9] Add Relacy tests to the build --- CMakeLists.txt | 1 + include/stdexec/__detail/__atomic.hpp | 2 +- test/CMakeLists.txt | 4 +++ test/rrd/CMakeLists.txt | 44 +++++++++++++++++++++++++++ test/rrd/README.md | 33 ++++++++++++++------ test/rrd/async_scope.cpp | 25 ++++++++++----- test/rrd/intrusive_mpsc_queue.cpp | 22 ++++++++++++-- test/rrd/split.cpp | 23 ++++++++++---- test/rrd/stdexec_relacy.hpp | 7 +++++ test/rrd/sync_wait.cpp | 2 +- 10 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 test/rrd/CMakeLists.txt create mode 100644 test/rrd/stdexec_relacy.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 54f6330bb..d79e2eaf3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ else() set(STDEXEC_BUILD_TESTS_DEFAULT OFF) endif() option(STDEXEC_BUILD_TESTS "Build stdexec tests" ${STDEXEC_BUILD_TESTS_DEFAULT}) +option(STDEXEC_BUILD_RELACY_TESTS "Build stdexec relacy tests" ${STDEXEC_BUILD_TESTS_DEFAULT}) # STDEXEC_BUILD_TESTS is used solely to configure CTest's BUILD_TESTING option, # which is CMake's preferred option for enabling testing when using CTest. diff --git a/include/stdexec/__detail/__atomic.hpp b/include/stdexec/__detail/__atomic.hpp index d97946b59..0b94ca77b 100644 --- a/include/stdexec/__detail/__atomic.hpp +++ b/include/stdexec/__detail/__atomic.hpp @@ -61,7 +61,7 @@ namespace STDEXEC::__std { using std::atomic_thread_fence; using std::atomic_signal_fence; -# if __cpp_lib_atomic_ref >= 2018'06L && !defined(STDEXEC_RELACY) +# if __cpp_lib_atomic_ref >= 2018'06L using std::atomic_ref; # else inline constexpr int __atomic_flag_map[] = { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 33e211362..6bbbd846d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -138,6 +138,10 @@ icm_add_build_failure_test( FOLDER test ) +if(STDEXEC_BUILD_RELACY_TESTS) + add_subdirectory(rrd) +endif() + # # Adding multiple tests with a glob # icm_glob_build_failure_tests( # PATTERN *_fail*.cpp diff --git a/test/rrd/CMakeLists.txt b/test/rrd/CMakeLists.txt new file mode 100644 index 000000000..74b7b39cc --- /dev/null +++ b/test/rrd/CMakeLists.txt @@ -0,0 +1,44 @@ +include(FetchContent) + +FetchContent_Declare( + relacy + GIT_REPOSITORY https://github.com/dvyukov/relacy + GIT_TAG master +) + +FetchContent_Populate(relacy) + +add_library(relacy INTERFACE) +target_include_directories(relacy INTERFACE + $ + $ + $ +) + +function(add_relacy_test target_name) + add_executable(${target_name} ${target_name}.cpp) + + target_link_libraries(${target_name} PRIVATE relacy) + target_include_directories(${target_name} PRIVATE ../../include) + target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + set_target_properties(${target_name} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF) + + add_test(NAME relacy-${target_name} COMMAND ${target_name}) +endfunction() + +set(relacy_tests + async_scope + intrusive_mpsc_queue + split + sync_wait +) + +foreach(test ${relacy_tests}) + add_relacy_test(${test}) +endforeach() + +# Target to build all relacy tests +add_custom_target(relacy-tests DEPENDS ${relacy_tests}) diff --git a/test/rrd/README.md b/test/rrd/README.md index 45d299028..799d343e4 100644 --- a/test/rrd/README.md +++ b/test/rrd/README.md @@ -15,13 +15,20 @@ STDEXEC library could needs to use `x.fetch_add(1)` to be compatible with Relacy ## Instructions -Run the following commands from within this directory (`./tests/rrd`). +Configure and build stdexec following the build instructions in the top level +[README.md](../../README.md). There are a couple relacy specific build and ctest +targets, though they are part of the standard build and ctest and will be run +automatically if cmake is configured with `-DSTDEXEC_BUILD_RELACY_TESTS=1`. +`STDEXEC_BUILD_RELACY_TESTS` is set by default for GCC today. + +Run the following on a Linux machine with GCC as the toolchain. ``` -git clone -b STDEXEC https://github.com/dvyukov/relacy -CXX=g++-11 make -j 4 -./build/split -./build/async_scope +mkdir build && cd build +cmake .. +make relacy-tests -j 4 +ctest -R relacy # Run all relacy tests +./test/rrd/sync_wait # Run a specific relacy test directly ``` ## Recommended use @@ -35,8 +42,16 @@ out a more stable build on all environments/compilers, we should revisit this. ## Supported platforms The STDEXEC Relacy tests have been verified to build and run on - * Linux based GCC+11 with libstdc++ (`x86_64`) - * Mac with Apple Clang 15 with libc++ (`x86_64`) + * Linux based GCC+11-14 with libstdc++ (`x86_64`) + * Mac with Apple Clang 15 and 17 with libc++ (`x86_64`) + +## Caveat + +Relacy relies on a less than robust approach to implement its runtime: it replaces +std:: names with its own versions, for example, std::atomic and std::mutex, as well +as pthread_* APIs. As libstdc++/libc++ evolve, newer versions may not be compatible with +Relacy. In these cases, changes to Relacy are needed to correctly intercept and replace +std:: names. -G++12 and newer are known to have issues that could be addressed with patches -to Relacy. +When the compilers and standard libraries release new versions, we will need to test the +new versions can compile the stdexec Relacy tests before enabling the new compiler. diff --git a/test/rrd/async_scope.cpp b/test/rrd/async_scope.cpp index e471a5769..f21bc44f0 100644 --- a/test/rrd/async_scope.cpp +++ b/test/rrd/async_scope.cpp @@ -1,17 +1,26 @@ -#include "../../relacy/relacy_cli.hpp" -#include "../../relacy/relacy_std.hpp" +/* + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include #include #include #include #include -#include - -using rl::nvar; -using rl::nvolatile; -using rl::mutex; - namespace ex = STDEXEC; using exec::async_scope; diff --git a/test/rrd/intrusive_mpsc_queue.cpp b/test/rrd/intrusive_mpsc_queue.cpp index 8fa3c4cc1..ec45dcdf5 100644 --- a/test/rrd/intrusive_mpsc_queue.cpp +++ b/test/rrd/intrusive_mpsc_queue.cpp @@ -1,4 +1,20 @@ -#include "../../relacy/relacy_std.hpp" +/* + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include #include @@ -314,7 +330,7 @@ struct mpsc_five_producers_ordered : rl::test_suite int { - int iterations = argc > 1 ? strtol(argv[1], nullptr, 10) : 500000; + int iterations = argc > 1 ? strtol(argv[1], nullptr, 10) : 250000; rl::test_params p; p.iteration_count = iterations; p.execution_depth_limit = 10000; @@ -341,7 +357,7 @@ auto main(int argc, char** argv) -> int { CHECK(rl::simulate(p)); // Beefy test... - p.iteration_count = 50000; + p.iteration_count = 5000; printf("Running mpsc_five_producers_ordered...\n"); CHECK(rl::simulate(p)); diff --git a/test/rrd/split.cpp b/test/rrd/split.cpp index 00482903d..d699d945f 100644 --- a/test/rrd/split.cpp +++ b/test/rrd/split.cpp @@ -1,13 +1,24 @@ -#include "../../relacy/relacy_cli.hpp" -#include "../../relacy/relacy_std.hpp" +/* + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include #include #include -using rl::nvar; -using rl::nvolatile; -using rl::mutex; - namespace ex = STDEXEC; struct split_bug : rl::test_suite { diff --git a/test/rrd/stdexec_relacy.hpp b/test/rrd/stdexec_relacy.hpp new file mode 100644 index 000000000..3e509bb97 --- /dev/null +++ b/test/rrd/stdexec_relacy.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include "../../relacy/relacy_std.hpp" + +namespace std { + template struct atomic_ref; +} diff --git a/test/rrd/sync_wait.cpp b/test/rrd/sync_wait.cpp index 1d78d9528..242db6665 100644 --- a/test/rrd/sync_wait.cpp +++ b/test/rrd/sync_wait.cpp @@ -15,7 +15,7 @@ * limitations under the License. */ -#include "../../relacy/relacy_std.hpp" +#include #include #include From c0720dd3873c71fe42d13538ea0c44067498b9a0 Mon Sep 17 00:00:00 2001 From: Chris Cotter Date: Wed, 4 Feb 2026 14:55:25 +0000 Subject: [PATCH 3/9] Default relacy only fofr GNU --- CMakeLists.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d79e2eaf3..3d688b63a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,6 @@ else() set(STDEXEC_BUILD_TESTS_DEFAULT OFF) endif() option(STDEXEC_BUILD_TESTS "Build stdexec tests" ${STDEXEC_BUILD_TESTS_DEFAULT}) -option(STDEXEC_BUILD_RELACY_TESTS "Build stdexec relacy tests" ${STDEXEC_BUILD_TESTS_DEFAULT}) # STDEXEC_BUILD_TESTS is used solely to configure CTest's BUILD_TESTING option, # which is CMake's preferred option for enabling testing when using CTest. @@ -151,6 +150,14 @@ else() set(stdexec_compiler_frontend ${CMAKE_CXX_COMPILER_ID}) endif() +# Build relacy tests by default only for GNU compiler and if tests are enabled +if(${STDEXEC_BUILD_TESTS} AND stdexec_compiler_frontend STREQUAL "GNU") + set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT ON) +else() + set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT OFF) +endif() +option(STDEXEC_BUILD_RELACY_TESTS "Build stdexec relacy tests" ${STDEXEC_BUILD_RELACY_TESTS_DEFAULT}) + set(stdexec_export_targets) # Define the main library From 0585125c5231e97065dfffff0ce631b9e583f538 Mon Sep 17 00:00:00 2001 From: Chris Cotter Date: Wed, 4 Feb 2026 14:56:41 +0000 Subject: [PATCH 4/9] Remove old Relacy Makefile --- test/rrd/Makefile | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 test/rrd/Makefile diff --git a/test/rrd/Makefile b/test/rrd/Makefile deleted file mode 100644 index b077640a5..000000000 --- a/test/rrd/Makefile +++ /dev/null @@ -1,43 +0,0 @@ -# User-customizable variables: -CXX ?= c++ -CXX_STD ?= c++20 -CXXFLAGS ?= -DSTDEXEC_RELACY -I relacy -I relacy/relacy/fakestd -O1 -std=$(CXX_STD) -I ../../include -I ../../test -g -DEPFLAGS ?= -MD -MF $(@).d -MP -MT $(@) -build_dir = build - -.SECONDARY: - -test_programs = split async_scope sync_wait intrusive_mpsc_queue - -test_exe_files = $(foreach name,$(test_programs),$(build_dir)/$(name)) - -exe_files = $(test_exe_files) -o_files = $(exe_files:=.cpp.o) - -ansi_term_csi = [ -ansi_term_bold = $(ansi_term_csi)1m -ansi_term_green = $(ansi_term_csi)32m -ansi_term_red = $(ansi_term_csi)31m -ansi_term_reset = $(ansi_term_csi)m - -COMPILE.cpp = $(CXX) $(DEPFLAGS) $(CXXFLAGS) -c -LINK.cpp = $(CXX) $(CXXFLAGS) - -.PHONY: all -all: tests - -.PHONY: tests -tests: $(test_exe_files) - -$(build_dir)/%: $(build_dir)/%.cpp.o - $(LINK.cpp) $(^) -o $(@) - -$(build_dir)/%.cpp.o: %.cpp - @mkdir -p $(dir $(@)) - $(COMPILE.cpp) -o $(@) $(<) - -.PHONY: clean -clean: - rm -fr -- $(build_dir)/ - --include $(o_files:=.d) From 2d595ad99206cefec5ba447b0ff949d09d672d31 Mon Sep 17 00:00:00 2001 From: Chris Cotter Date: Wed, 4 Feb 2026 15:01:09 +0000 Subject: [PATCH 5/9] Simplify tests --- test/rrd/intrusive_mpsc_queue.cpp | 48 +++++++++++++------------------ 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/test/rrd/intrusive_mpsc_queue.cpp b/test/rrd/intrusive_mpsc_queue.cpp index ec45dcdf5..3e3e2ed87 100644 --- a/test/rrd/intrusive_mpsc_queue.cpp +++ b/test/rrd/intrusive_mpsc_queue.cpp @@ -59,13 +59,12 @@ struct mpsc_single_producer_consumer : rl::test_suite { test_queue queue; test_node nodes[4] = {test_node{1}, test_node{2}, test_node{3}, test_node{4}}; - std::atomic consumed_count{0}; - std::atomic seen[4]; + int consumed_count{0}; + bool seen[4]; void before() { - consumed_count.store(0, std::memory_order_relaxed); for (int i = 0; i < 4; ++i) { - seen[i].store(false, std::memory_order_relaxed); + seen[i] = false; } } @@ -80,25 +79,23 @@ struct mpsc_two_producers : rl::test_suite { queue.push_back(&nodes[3]); } else { // Consumer - int count = 0; - while (count < 4) { + while (consumed_count < 4) { test_node* node = queue.pop_front(); if (node) { int idx = node->value_ - 1; RL_ASSERT(idx >= 0 && idx < 4); - bool was_seen = seen[idx].exchange(true, std::memory_order_relaxed); + bool was_seen = std::exchange(seen[idx], true); RL_ASSERT(!was_seen); // Each node should be seen exactly once - ++count; + ++consumed_count; } } - consumed_count.store(count, std::memory_order_relaxed); } } void after() { - RL_ASSERT(consumed_count.load() == 4); + RL_ASSERT(consumed_count == 4); for (int i = 0; i < 4; ++i) { - RL_ASSERT(seen[i].load()); + RL_ASSERT(seen[i]); } } }; @@ -202,13 +199,12 @@ struct mpsc_five_prod_one_cons : rl::test_suite { // Producer 4: values 40000-40001 test_node{40000}, test_node{40001} }; - std::atomic consumed_count{0}; - std::atomic seen[10]; + int consumed_count{0}; + bool seen[10]; void before() { - consumed_count.store(0, std::memory_order_relaxed); for (int i = 0; i < 10; ++i) { - seen[i].store(false, std::memory_order_relaxed); + seen[i] = false; } } @@ -220,8 +216,7 @@ struct mpsc_five_prod_one_cons : rl::test_suite { queue.push_back(&nodes[base_idx + 1]); } else { // Consumer thread (5) - std::atomic count = 0; - while (count < 10) { + while (consumed_count < 10) { test_node* node = queue.pop_front(); if (node) { // Map value to index @@ -234,19 +229,18 @@ struct mpsc_five_prod_one_cons : rl::test_suite { idx = producer_id * 2 + item_in_producer; } RL_ASSERT(idx >= 0 && idx < 10); - bool was_seen = seen[idx].exchange(true, std::memory_order_relaxed); + bool was_seen = std::exchange(seen[idx], true); RL_ASSERT(!was_seen); // Each node should be seen exactly once - count.fetch_add(1); + ++consumed_count; } } - consumed_count.store(count, std::memory_order_relaxed); } } void after() { - RL_ASSERT(consumed_count.load() == 10); + RL_ASSERT(consumed_count == 10); for (int i = 0; i < 10; ++i) { - RL_ASSERT(seen[i].load()); + RL_ASSERT(seen[i]); } } }; @@ -258,11 +252,10 @@ struct mpsc_five_producers_ordered : rl::test_suite consumed_count{0}; + int consumed_count{0}; int consumed_values[TOTAL_ITEMS]; void before() { - consumed_count.store(0, std::memory_order_relaxed); for (int i = 0; i < TOTAL_ITEMS; ++i) { consumed_values[i] = -1; // Initialize nodes with their values @@ -279,19 +272,18 @@ struct mpsc_five_producers_ordered : rl::test_suitevalue_; - ++count; + ++consumed_count; } } - consumed_count.store(count, std::memory_order_relaxed); } } void after() { - RL_ASSERT(consumed_count.load() == TOTAL_ITEMS); + RL_ASSERT(consumed_count == TOTAL_ITEMS); // Check that each value appears exactly once bool seen[TOTAL_ITEMS + 1] = {false}; // values are 1-500, so need 501 elements From 3b6648bb7ce30eaa9e86ac18f68b624f4e52df1a Mon Sep 17 00:00:00 2001 From: Maikel Nadolski Date: Thu, 5 Feb 2026 08:34:49 +0100 Subject: [PATCH 6/9] Improve implementation to be near to Dmitry's --- include/exec/timed_thread_scheduler.hpp | 2 +- .../__detail/__intrusive_mpsc_queue.hpp | 73 +++++++++++-------- test/rrd/intrusive_mpsc_queue.cpp | 6 +- .../detail/test_intrusive_mpsc_queue.cpp | 2 +- 4 files changed, 49 insertions(+), 34 deletions(-) diff --git a/include/exec/timed_thread_scheduler.hpp b/include/exec/timed_thread_scheduler.hpp index 7f4fa83bd..fec93bb7c 100644 --- a/include/exec/timed_thread_scheduler.hpp +++ b/include/exec/timed_thread_scheduler.hpp @@ -53,7 +53,7 @@ namespace exec { , set_value_{set_value} { } - STDEXEC::__std::atomic next_{nullptr}; + STDEXEC::__std::atomic next_{nullptr}; command_type command_; void (*set_value_)(timed_thread_operation_base*) noexcept; }; diff --git a/include/stdexec/__detail/__intrusive_mpsc_queue.hpp b/include/stdexec/__detail/__intrusive_mpsc_queue.hpp index e980e9b57..e349e3b3c 100644 --- a/include/stdexec/__detail/__intrusive_mpsc_queue.hpp +++ b/include/stdexec/__detail/__intrusive_mpsc_queue.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) Dmitiy V'jukov + * Copyright (c) Dmitry V'jukov * Copyright (c) 2024 Maikel Nadolski * Copyright (c) 2024 NVIDIA Corporation * @@ -24,6 +24,8 @@ #include "__atomic.hpp" +#include "stdexec/__detail/__config.hpp" + #include "./__spin_loop_pause.hpp" namespace STDEXEC { @@ -32,13 +34,13 @@ namespace STDEXEC { // _Node must be default_initializable only for the queue to construct an // internal "stub" node - only the _Next data element is accessed internally. - template _Node::* _Next> + template _Node::* _Next> requires __std::default_initializable<_Node> class __intrusive_mpsc_queue<_Next> { - __std::atomic __back_{&__stub_}; - __std::atomic __head_{&__stub_}; - _Node __stub_; + __std::atomic<_Node*> __head_{&__stub_}; + _Node* __tail_{&__stub_}; + _Node __stub_{}; public: @@ -47,38 +49,51 @@ namespace STDEXEC { } constexpr auto push_back(_Node* __new_node) noexcept -> bool { - (__new_node->*_Next).store(nullptr, __std::memory_order_release); - _Node* __prev = static_cast<_Node*>( - __head_.exchange(static_cast(__new_node), __std::memory_order_acq_rel) - ); - bool was_stub = __prev == &__stub_; - (__prev->*_Next).store(static_cast(__new_node), __std::memory_order_release); - return was_stub; + (__new_node->*_Next).store(nullptr, __std::memory_order_relaxed); + _Node* __prev = __head_.exchange(__new_node, __std::memory_order_acq_rel); + (__prev->*_Next).store(__new_node, __std::memory_order_release); + return __prev == &__stub_; } constexpr auto pop_front() noexcept -> _Node* { - _Node* __back = static_cast<_Node*>(__back_.load(__std::memory_order_relaxed)); - _Node* __next = static_cast<_Node*>((__back->*_Next).load(__std::memory_order_acquire)); - if (__back == &__stub_) { - if (nullptr == __next) - return nullptr; - __back_.store(static_cast(__next), __std::memory_order_relaxed); - __back = __next; - __next = static_cast<_Node*>((__next->*_Next).load(__std::memory_order_acquire)); + _Node* __tail = this->__tail_; + STDEXEC_ASSERT(__tail != nullptr); + _Node* __next = (__tail->*_Next).load(__std::memory_order_acquire); + // If tail is pointing to the stub node we need to advance it once more + if (&__stub_ == __tail) { + if (nullptr == __next) { + return nullptr; + } + this->__tail_ = __next; + __tail = __next; + __next = (__next->*_Next).load(__std::memory_order_acquire); } - if (__next) { - __back_.store(static_cast(__next), __std::memory_order_relaxed); - return __back; + // Normal case: there is a next node and we can just advance the tail + if (nullptr != __next) { + this->__tail_ = __next; + return __tail; } - _Node* __head = static_cast<_Node*>(__head_.load(__std::memory_order_relaxed)); - if (__back != __head) + // Next is nullptr here means that either: + // 1) There are no more nodes in the queue + // 2) A producer is in the middle of adding a new node + const _Node* __head = this->__head_.load(__std::memory_order_acquire); + // A producer is in the middle of adding a new node + // we cannot return tail as we cannot link the next node yet + if (__tail != __head) { return nullptr; + } + // No more nodes in the queue - we need to insert a stub node + // to be able to link to an eventual empty state (or new nodes) push_back(&__stub_); - __next = static_cast<_Node*>((__back->*_Next).load(__std::memory_order_acquire)); - if (__next) { - __back_.store(static_cast(__next), __std::memory_order_relaxed); - return __back; + // Now re-attempt to load next + __next = (__tail->*_Next).load(__std::memory_order_acquire); + if (nullptr != __next) { + // Successfully linked either a new node or the stub node + this->__tail_ = __next; + return __tail; } + // A producer is in the middle of adding a new node since next is still nullptr + // and not our stub node, thus we cannot link the next node yet return nullptr; } }; diff --git a/test/rrd/intrusive_mpsc_queue.cpp b/test/rrd/intrusive_mpsc_queue.cpp index 3e3e2ed87..0646d965b 100644 --- a/test/rrd/intrusive_mpsc_queue.cpp +++ b/test/rrd/intrusive_mpsc_queue.cpp @@ -19,7 +19,7 @@ #include struct test_node { - std::atomic next_{nullptr}; + std::atomic next_{nullptr}; int value_{0}; test_node() = default; @@ -271,11 +271,11 @@ struct mpsc_five_producers_ordered : rl::test_suitevalue_; + consumed_values[consumed_count] = node->value_; ++consumed_count; } } diff --git a/test/stdexec/detail/test_intrusive_mpsc_queue.cpp b/test/stdexec/detail/test_intrusive_mpsc_queue.cpp index 1ab81824a..ebeac82d2 100644 --- a/test/stdexec/detail/test_intrusive_mpsc_queue.cpp +++ b/test/stdexec/detail/test_intrusive_mpsc_queue.cpp @@ -10,7 +10,7 @@ namespace { struct test_node { - std::atomic next_{nullptr}; + std::atomic next_{nullptr}; int value_{0}; test_node() = default; From 0e9451c34a733859065e21c362d646f388340166 Mon Sep 17 00:00:00 2001 From: Chris Cotter Date: Sun, 8 Feb 2026 03:43:08 +0000 Subject: [PATCH 7/9] Remove unused line, fix CI --- CMakeLists.txt | 3 ++- test/rrd/intrusive_mpsc_queue.cpp | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b4ce1684f..072d8bb7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,7 +151,8 @@ else() endif() # Build relacy tests by default only for GNU compiler and if tests are enabled -if(${STDEXEC_BUILD_TESTS} AND stdexec_compiler_frontend STREQUAL "GNU") +# It will take work to get other platforms to work in the future. +if(${STDEXEC_BUILD_TESTS} AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT ON) else() set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT OFF) diff --git a/test/rrd/intrusive_mpsc_queue.cpp b/test/rrd/intrusive_mpsc_queue.cpp index 0646d965b..422aa8623 100644 --- a/test/rrd/intrusive_mpsc_queue.cpp +++ b/test/rrd/intrusive_mpsc_queue.cpp @@ -271,7 +271,6 @@ struct mpsc_five_producers_ordered : rl::test_suite Date: Sun, 8 Feb 2026 04:02:34 +0000 Subject: [PATCH 8/9] CI fixes - Temp workaround for relacy to compile in CI - Do not build Relacy tests with sanitizers by default --- CMakeLists.txt | 13 ++++++++++--- test/rrd/shared_mutex | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 test/rrd/shared_mutex diff --git a/CMakeLists.txt b/CMakeLists.txt index 072d8bb7e..0757319e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,10 +152,17 @@ endif() # Build relacy tests by default only for GNU compiler and if tests are enabled # It will take work to get other platforms to work in the future. -if(${STDEXEC_BUILD_TESTS} AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") - set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT ON) +# Additionally, do not build the Relacy tests when a sanitizer is active. TSAN +# doesn't do anything since Relacy simulates threads with fibers anyway. Relacy +# has primtive (compared to ASAN) use-after-free detection which doesn't compose +# with ASAN. +if( + ${STDEXEC_BUILD_TESTS} AND + CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND + NOT CMAKE_CXX_FLAGS MATCHES "-fsanitize") + set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT ON) else() - set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT OFF) + set(STDEXEC_BUILD_RELACY_TESTS_DEFAULT OFF) endif() option(STDEXEC_BUILD_RELACY_TESTS "Build stdexec relacy tests" ${STDEXEC_BUILD_RELACY_TESTS_DEFAULT}) diff --git a/test/rrd/shared_mutex b/test/rrd/shared_mutex new file mode 100644 index 000000000..48091e683 --- /dev/null +++ b/test/rrd/shared_mutex @@ -0,0 +1,15 @@ +// Temporary workaround until upstream Relacy provides a fakestd header for shared_mutex +// stdexec pulls in shared_mutex through TBB, +// In file included from /usr/include/c++/13/shared_mutex:42, +// from /usr/include/c++/13/memory_resource:62, +// from /usr/include/oneapi/tbb/cache_aligned_allocator.h:26, +// from /usr/include/oneapi/tbb/partitioner.h:46, +// from /usr/include/oneapi/tbb/parallel_for.h:27, +// from /usr/include/tbb/parallel_for.h:17, +// from /usr/include/c++/13/pstl/parallel_backend_tbb.h:20, +// from /usr/include/c++/13/pstl/parallel_backend.h:20, +// from /usr/include/c++/13/pstl/algorithm_impl.h:22, +// from /usr/include/c++/13/pstl/glue_execution_defs.h:50, +// from /usr/include/c++/13/execution:34, +// from /home/coder/stdexec/test/rrd/../../include/exec/../stdexec/__detail/__execution_legacy.hpp:21, +#pragma once From b33534f383bdb1e22cacdd2e7b96bc27bda08656 Mon Sep 17 00:00:00 2001 From: Chris Cotter Date: Sun, 8 Feb 2026 17:58:44 -0500 Subject: [PATCH 9/9] Use std::thread --- test/stdexec/detail/test_intrusive_mpsc_queue.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/stdexec/detail/test_intrusive_mpsc_queue.cpp b/test/stdexec/detail/test_intrusive_mpsc_queue.cpp index ebeac82d2..95c84376b 100644 --- a/test/stdexec/detail/test_intrusive_mpsc_queue.cpp +++ b/test/stdexec/detail/test_intrusive_mpsc_queue.cpp @@ -42,14 +42,14 @@ namespace { std::set consumed_addrs; - std::jthread producer1([&]() { + std::thread producer1([&]() { for (int i = 0; i < num_items_per_producer; ++i) { queue.push_back(nodes1[i].get()); produced_count.fetch_add(1, std::memory_order_relaxed); } }); - std::jthread producer2([&]() { + std::thread producer2([&]() { for (int i = 0; i < num_items_per_producer; ++i) { queue.push_back(nodes2[i].get()); produced_count.fetch_add(1, std::memory_order_relaxed); @@ -57,7 +57,7 @@ namespace { }); std::set consumed; - std::jthread consumer([&]() { + std::thread consumer([&]() { int count = 0; while (count < total_items) { test_node* node = queue.pop_front();