spec-and-tsan #1
@@ -73,6 +73,13 @@ jobs:
|
|||||||
if: ${{ !failure() && !cancelled() && (needs.changes.outputs.code == 'true' || needs.changes.outputs.dockerfile == 'true') }}
|
if: ${{ !failure() && !cancelled() && (needs.changes.outputs.code == 'true' || needs.changes.outputs.dockerfile == 'true') }}
|
||||||
uses: ./.gitea/workflows/test.yaml
|
uses: ./.gitea/workflows/test.yaml
|
||||||
|
|
||||||
|
# ThreadSanitizer run for the lock-free Channel<T>. Same trigger conditions as
|
||||||
|
# test (code or image changed); runs in parallel with test.
|
||||||
|
tsan:
|
||||||
|
needs: [changes, docker]
|
||||||
|
if: ${{ !failure() && !cancelled() && (needs.changes.outputs.code == 'true' || needs.changes.outputs.dockerfile == 'true') }}
|
||||||
|
uses: ./.gitea/workflows/tsan.yaml
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
needs: [changes, docker]
|
needs: [changes, docker]
|
||||||
if: ${{ !failure() && !cancelled() && github.ref == 'refs/heads/master' && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.dockerfile == 'true') }}
|
if: ${{ !failure() && !cancelled() && github.ref == 'refs/heads/master' && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.dockerfile == 'true') }}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: '🧵 ThreadSanitizer'
|
||||||
|
|
||||||
|
# Reusable workflow: builds the channel stress suite with ThreadSanitizer and
|
||||||
|
# runs it. This is the dynamic half of verifying the lock-free SPSC Channel<T>
|
||||||
|
# (the static half is the CDSChecker model-check harness in verify/).
|
||||||
|
#
|
||||||
|
# Triggering and path filtering are owned by ci.yaml (the orchestrator), which
|
||||||
|
# calls this only when code changed. workflow_dispatch is kept for manual runs.
|
||||||
|
#
|
||||||
|
# Runs in the prebuilt builder image (gcc:14), which already ships libtsan — no
|
||||||
|
# package installs at job time.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tsan:
|
||||||
|
runs-on: linux/amd64
|
||||||
|
container:
|
||||||
|
image: gitea.tourolle.paris/dtourolle/kpnpp-builder:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: tsan-${{ github.run_id }}
|
||||||
|
|
||||||
|
- name: Cache FetchContent dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cmake/fetchcontent
|
||||||
|
key: cmake-fetchcontent-${{ hashFiles('**/CMakeLists.txt') }}
|
||||||
|
restore-keys: cmake-fetchcontent-
|
||||||
|
|
||||||
|
- name: Configure (TSan)
|
||||||
|
working-directory: tsan-${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
cmake -S . -B build \
|
||||||
|
-G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Debug \
|
||||||
|
-DKPN_SANITIZER=thread \
|
||||||
|
-DKPN_BUILD_TESTS=ON \
|
||||||
|
-DKPN_BUILD_EXAMPLES=OFF \
|
||||||
|
-DKPN_BUILD_PYTHON=OFF \
|
||||||
|
-DFETCHCONTENT_BASE_DIR=$HOME/.cmake/fetchcontent
|
||||||
|
|
||||||
|
- name: Build (TSan)
|
||||||
|
working-directory: tsan-${{ github.run_id }}
|
||||||
|
run: cmake --build build --parallel --target kpn_tests kpn_tests_stress
|
||||||
|
|
||||||
|
- name: Run stress suite under TSan
|
||||||
|
working-directory: tsan-${{ github.run_id }}
|
||||||
|
# halt_on_error=1 makes the first detected race fail the job; the report
|
||||||
|
# (with both stacks) is printed to the log. second_deadlock_stack gives
|
||||||
|
# the full picture for lock-order issues.
|
||||||
|
env:
|
||||||
|
TSAN_OPTIONS: "halt_on_error=1 second_deadlock_stack=1"
|
||||||
|
run: ./build/tests/kpn_tests_stress
|
||||||
|
|
||||||
|
- name: Run unit tests under TSan
|
||||||
|
working-directory: tsan-${{ github.run_id }}
|
||||||
|
env:
|
||||||
|
TSAN_OPTIONS: "halt_on_error=1 second_deadlock_stack=1"
|
||||||
|
run: ./build/tests/kpn_tests
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: rm -rf tsan-${{ github.run_id }}
|
||||||
@@ -10,6 +10,30 @@ option(KPN_BUILD_PYTHON "Build Python bindings (requires nanobind)" ON)
|
|||||||
option(KPN_BUILD_EXAMPLES "Build examples" ON)
|
option(KPN_BUILD_EXAMPLES "Build examples" ON)
|
||||||
option(KPN_WEB_DEBUG "Enable web debug UI (cpp-httplib)" OFF)
|
option(KPN_WEB_DEBUG "Enable web debug UI (cpp-httplib)" OFF)
|
||||||
|
|
||||||
|
# Sanitizer build. Empty = off. Accepts "thread", "address", "undefined",
|
||||||
|
# or a combination like "address,undefined". Applied to all kpn targets via
|
||||||
|
# the kpn_sanitizer_flags() helper below.
|
||||||
|
#
|
||||||
|
# The lock-free SPSC Channel<T> (include/kpn/channel.hpp) has hand-reasoned
|
||||||
|
# acquire/release ordering; -DKPN_SANITIZER=thread + the channel stress test
|
||||||
|
# (tests/test_channel_stress.cpp) is the dynamic half of verifying it. The
|
||||||
|
# static half is the CDSChecker model-check harness (see verify/).
|
||||||
|
set(KPN_SANITIZER "" CACHE STRING
|
||||||
|
"Build with sanitizer: thread | address | undefined | <combo> (empty = off)")
|
||||||
|
|
||||||
|
# Translate KPN_SANITIZER into compile/link flags. No-op when empty.
|
||||||
|
function(kpn_sanitizer_flags out_var)
|
||||||
|
if(KPN_SANITIZER)
|
||||||
|
set(${out_var}
|
||||||
|
-fsanitize=${KPN_SANITIZER}
|
||||||
|
-fno-omit-frame-pointer
|
||||||
|
-g
|
||||||
|
PARENT_SCOPE)
|
||||||
|
else()
|
||||||
|
set(${out_var} "" PARENT_SCOPE)
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
# ── Core library (header-only) ────────────────────────────────────────────────
|
# ── Core library (header-only) ────────────────────────────────────────────────
|
||||||
add_library(kpn INTERFACE)
|
add_library(kpn INTERFACE)
|
||||||
target_include_directories(kpn INTERFACE
|
target_include_directories(kpn INTERFACE
|
||||||
|
|||||||
+30
-1
@@ -43,6 +43,35 @@ target_link_libraries(kpn_tests PRIVATE
|
|||||||
GTest::gtest
|
GTest::gtest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── Channel stress suite (separate executable) ────────────────────────────────
|
||||||
|
# Contended SPSC tests for the lock-free Channel<T>. Kept out of kpn_tests
|
||||||
|
# because each case runs many reps / tens of thousands of items and is slow.
|
||||||
|
# Most valuable under -DKPN_SANITIZER=thread, but correct (and run) without it.
|
||||||
|
add_executable(kpn_tests_stress test_channel_stress.cpp)
|
||||||
|
target_link_libraries(kpn_tests_stress PRIVATE kpn Catch2::Catch2WithMain)
|
||||||
|
|
||||||
|
# ── Sanitizer flags ───────────────────────────────────────────────────────────
|
||||||
|
# kpn_sanitizer_flags() is defined in the top-level CMakeLists and is a no-op
|
||||||
|
# unless -DKPN_SANITIZER=... is set. Sanitizer must be on both compile and link.
|
||||||
|
kpn_sanitizer_flags(_kpn_san)
|
||||||
|
if(_kpn_san)
|
||||||
|
foreach(_t kpn_tests kpn_tests_stress)
|
||||||
|
target_compile_options(${_t} PRIVATE ${_kpn_san})
|
||||||
|
target_link_options(${_t} PRIVATE ${_kpn_san})
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
include(Catch)
|
include(Catch)
|
||||||
catch_discover_tests(kpn_tests)
|
|
||||||
|
# DISCOVERY_MODE PRE_TEST defers test enumeration to `ctest` run time. The
|
||||||
|
# default (POST_BUILD) runs each test binary during the build to list its
|
||||||
|
# cases — which fails a sanitizer build: a TSan/ASan binary needs a fixed
|
||||||
|
# address-space layout and aborts on startup ("unexpected memory mapping")
|
||||||
|
# under the container's ASLR, breaking the build before any test runs. The
|
||||||
|
# tsan.yaml job invokes the binaries directly (not via ctest), so deferring
|
||||||
|
# discovery costs nothing there and keeps `ctest` working for normal builds.
|
||||||
|
catch_discover_tests(kpn_tests DISCOVERY_MODE PRE_TEST)
|
||||||
|
# Register the stress suite under its own label so CI can run / time it
|
||||||
|
# separately from the fast unit tests.
|
||||||
|
catch_discover_tests(kpn_tests_stress DISCOVERY_MODE PRE_TEST PROPERTIES LABELS "stress")
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
// Contended stress tests for the lock-free SPSC Channel<T>.
|
||||||
|
//
|
||||||
|
// The other channel tests (test_channel.cpp) are single-threaded or use a
|
||||||
|
// single 20 ms sleep to order two threads — they never actually contend on the
|
||||||
|
// ring, so they exercise neither the memory-ordering pairing nor the
|
||||||
|
// spin/futex/lost-wakeup logic in pop().
|
||||||
|
//
|
||||||
|
// These tests are written to be run under ThreadSanitizer:
|
||||||
|
//
|
||||||
|
// cmake -B build -DKPN_SANITIZER=thread -DKPN_BUILD_EXAMPLES=OFF -DKPN_BUILD_PYTHON=OFF
|
||||||
|
// cmake --build build --target kpn_tests_tsan
|
||||||
|
// ./build/tests/kpn_tests_tsan
|
||||||
|
//
|
||||||
|
// They are also valid (and meaningful) without a sanitizer: the value/sequence
|
||||||
|
// assertions catch lost or duplicated items regardless of build flags. TSan
|
||||||
|
// adds detection of the underlying data race even on runs where the race did
|
||||||
|
// not corrupt observable state.
|
||||||
|
//
|
||||||
|
// Channel<T> is SPSC: exactly one producer thread and one consumer thread per
|
||||||
|
// channel. Every scenario below honours that contract.
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <kpn/channel.hpp>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace kpn;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Repeat each scenario enough times that rare interleavings (spin window just
|
||||||
|
// missing / just catching the next push, disable landing inside the futex
|
||||||
|
// wait) actually occur across a run. Kept modest so a TSan run stays minutes,
|
||||||
|
// not hours.
|
||||||
|
constexpr int kReps = 200;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("SPSC: every pushed item is popped exactly once, in order", "[channel][stress]") {
|
||||||
|
// Small capacity forces frequent full/empty transitions, so both the
|
||||||
|
// producer's overflow-retry and the consumer's spin->futex path are hit
|
||||||
|
// many times. The producer retries on overflow rather than dropping, so
|
||||||
|
// the consumer must observe a strictly contiguous 0..N-1 sequence.
|
||||||
|
constexpr int N = 50'000;
|
||||||
|
Channel<int> ch(/*capacity=*/4, /*spin_count=*/16);
|
||||||
|
|
||||||
|
std::thread producer([&] {
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
for (;;) {
|
||||||
|
try { ch.push(i); break; }
|
||||||
|
catch (const ChannelOverflowError&) { std::this_thread::yield(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
int expected = 0;
|
||||||
|
bool in_order = true;
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
int v = ch.pop();
|
||||||
|
if (v != expected) in_order = false;
|
||||||
|
++expected;
|
||||||
|
}
|
||||||
|
producer.join();
|
||||||
|
|
||||||
|
REQUIRE(in_order);
|
||||||
|
REQUIRE(expected == N);
|
||||||
|
REQUIRE(ch.size() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SPSC: tight empty<->non-empty transitions exercise spin/futex boundary",
|
||||||
|
"[channel][stress]") {
|
||||||
|
// spin_count=0 forces every empty pop() straight into atomic::wait, so this
|
||||||
|
// hammers the lost-wakeup guard (snapshot wake_, re-check tail_, then wait).
|
||||||
|
// The producer pushes one item then waits to go empty again, maximising the
|
||||||
|
// number of empty->non-empty edges relative to item count.
|
||||||
|
constexpr int N = 20'000;
|
||||||
|
Channel<int> ch(/*capacity=*/2, /*spin_count=*/0);
|
||||||
|
|
||||||
|
std::thread producer([&] {
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
for (;;) {
|
||||||
|
try { ch.push(i); break; }
|
||||||
|
catch (const ChannelOverflowError&) { std::this_thread::yield(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
long sum = 0;
|
||||||
|
for (int i = 0; i < N; ++i) sum += ch.pop();
|
||||||
|
producer.join();
|
||||||
|
|
||||||
|
// Sum of 0..N-1 — detects any lost or duplicated item.
|
||||||
|
REQUIRE(sum == static_cast<long>(N) * (N - 1) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SPSC: disable() while consumer is blocked in pop() unblocks cleanly",
|
||||||
|
"[channel][stress]") {
|
||||||
|
// The data race of record: consumer blocked in pop() (spinning or parked in
|
||||||
|
// the futex) while the owner thread calls disable(). pop() must observe the
|
||||||
|
// close and throw ChannelClosedError — it must not hang and must not read
|
||||||
|
// past the ring. Repeated so disable() lands at many points in pop()'s loop.
|
||||||
|
for (int rep = 0; rep < kReps; ++rep) {
|
||||||
|
Channel<int> ch(/*capacity=*/4, /*spin_count=*/8);
|
||||||
|
std::atomic<bool> threw{false};
|
||||||
|
std::atomic<bool> finished{false};
|
||||||
|
|
||||||
|
std::thread consumer([&] {
|
||||||
|
try {
|
||||||
|
ch.pop(); // empty channel: will block
|
||||||
|
} catch (const ChannelClosedError&) {
|
||||||
|
threw.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
finished.store(true, std::memory_order_relaxed);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give the consumer a chance to reach the wait, then close.
|
||||||
|
std::this_thread::sleep_for(50us);
|
||||||
|
ch.disable();
|
||||||
|
|
||||||
|
consumer.join();
|
||||||
|
REQUIRE(finished.load());
|
||||||
|
REQUIRE(threw.load());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SPSC: producer racing a disable() never throws and never hangs",
|
||||||
|
"[channel][stress]") {
|
||||||
|
// Mirror of the above from the producer side: push() racing disable() must
|
||||||
|
// either enqueue or silently drop, never throw ChannelClosedError and never
|
||||||
|
// wedge. Overflow is still a legal outcome (full accepting channel) and is
|
||||||
|
// tolerated here.
|
||||||
|
for (int rep = 0; rep < kReps; ++rep) {
|
||||||
|
Channel<int> ch(/*capacity=*/8, /*spin_count=*/8);
|
||||||
|
std::atomic<bool> bad{false};
|
||||||
|
|
||||||
|
std::thread producer([&] {
|
||||||
|
for (int i = 0; i < 1000; ++i) {
|
||||||
|
try { ch.push(i); }
|
||||||
|
catch (const ChannelOverflowError&) { /* legal: full */ }
|
||||||
|
catch (...) { bad.store(true, std::memory_order_relaxed); break; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(20us);
|
||||||
|
ch.disable(); // owner closes mid-stream
|
||||||
|
producer.join();
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bad.load());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SPSC: push_callback fires on each empty->non-empty transition",
|
||||||
|
"[channel][stress]") {
|
||||||
|
// The empty->non-empty callback ([channel.hpp] was_empty branch) is read by
|
||||||
|
// the consumer-side notification path. Run it under contention to make sure
|
||||||
|
// the was_empty detection isn't torn by a concurrent pop().
|
||||||
|
Channel<int> ch(/*capacity=*/4, /*spin_count=*/4);
|
||||||
|
std::atomic<int> callbacks{0};
|
||||||
|
ch.set_push_callback([&] { callbacks.fetch_add(1, std::memory_order_relaxed); });
|
||||||
|
|
||||||
|
constexpr int N = 10'000;
|
||||||
|
std::thread producer([&] {
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
for (;;) {
|
||||||
|
try { ch.push(i); break; }
|
||||||
|
catch (const ChannelOverflowError&) { std::this_thread::yield(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int i = 0; i < N; ++i) (void)ch.pop();
|
||||||
|
producer.join();
|
||||||
|
|
||||||
|
// At least one transition, at most one per item; mainly we assert the run
|
||||||
|
// completed without TSan flagging a race on push_callback_/was_empty.
|
||||||
|
REQUIRE(callbacks.load() >= 1);
|
||||||
|
REQUIRE(callbacks.load() <= N);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the out-of-band sentinel guarantees under contention — and what it does
|
||||||
|
// not. push_sentinel() publishes has_eof_ (release) after the producer's N ring
|
||||||
|
// pushes; a consumer that observes has_eof_ (acquire) therefore also observes
|
||||||
|
// every value pushed before it. What these tests assert:
|
||||||
|
//
|
||||||
|
// * Losslessness — every value 0..N-1 is delivered exactly once (contiguous,
|
||||||
|
// no gaps, no duplicates) and the sentinel is delivered exactly once. This
|
||||||
|
// is the invariant that must hold on every run; a broken acquire/release
|
||||||
|
// pairing would surface as a lost/duplicated value or (under TSan) a data
|
||||||
|
// race on has_eof_/eof_value_.
|
||||||
|
//
|
||||||
|
// What they deliberately do NOT assert is that the sentinel is the *strictly
|
||||||
|
// last* item popped. pop() checks emptiness (h == t) using a tail_ snapshot
|
||||||
|
// taken at the top of its loop; the producer can push more values *and* the
|
||||||
|
// sentinel in the window before take_sentinel() runs, so the consumer may
|
||||||
|
// surface the sentinel with a few real values still queued behind it. Those
|
||||||
|
// values are not lost — a consumer that keeps draining still receives them —
|
||||||
|
// but "sentinel arrives dead last" is not a property the channel promises, so
|
||||||
|
// asserting it would be flaky. We track how many values trailed the sentinel
|
||||||
|
// for visibility without failing on it.
|
||||||
|
|
||||||
|
TEST_CASE("SPSC: sentinel and all values survive contention (blocking pop)",
|
||||||
|
"[channel][stress]") {
|
||||||
|
constexpr int N = 20'000;
|
||||||
|
constexpr int SENTINEL = -1;
|
||||||
|
|
||||||
|
for (int rep = 0; rep < kReps; ++rep) {
|
||||||
|
// Small ring + tiny spin window so the ring is frequently empty exactly
|
||||||
|
// when the sentinel is published — the interleaving under test.
|
||||||
|
Channel<int> ch(/*capacity=*/4, /*spin_count=*/8);
|
||||||
|
|
||||||
|
std::thread producer([&] {
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
for (;;) {
|
||||||
|
try { ch.push(i); break; }
|
||||||
|
catch (const ChannelOverflowError&) { std::this_thread::yield(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ch.push_sentinel(SENTINEL); // must-deliver, never overflows/blocks
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<bool> seen(N, false);
|
||||||
|
int values = 0;
|
||||||
|
int sentinels = 0;
|
||||||
|
bool duplicate = false;
|
||||||
|
// Drain until the sentinel AND all N values have been received; the
|
||||||
|
// sentinel may arrive before the last few values (see note above).
|
||||||
|
while (values < N || sentinels == 0) {
|
||||||
|
int v = ch.pop();
|
||||||
|
if (v == SENTINEL) { ++sentinels; continue; }
|
||||||
|
if (seen[v]) duplicate = true; else seen[v] = true;
|
||||||
|
++values;
|
||||||
|
}
|
||||||
|
producer.join();
|
||||||
|
|
||||||
|
REQUIRE_FALSE(duplicate);
|
||||||
|
REQUIRE(values == N); // every value delivered exactly once
|
||||||
|
REQUIRE(sentinels == 1); // sentinel delivered exactly once
|
||||||
|
REQUIRE(ch.size() == 0);
|
||||||
|
REQUIRE(ch.approx_size() == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SPSC: sentinel and all values survive contention (try_pop_now)",
|
||||||
|
"[channel][stress]") {
|
||||||
|
// The pool-node consume path is try_pop_now(), not pop(): it must surface
|
||||||
|
// the out-of-band sentinel once the ring is observed empty. The consumer
|
||||||
|
// spins with no sleeps, racing the producer at full tilt across the
|
||||||
|
// empty-ring boundary where take_sentinel() is reached.
|
||||||
|
constexpr int N = 20'000;
|
||||||
|
constexpr int SENTINEL = -1;
|
||||||
|
|
||||||
|
for (int rep = 0; rep < kReps; ++rep) {
|
||||||
|
Channel<int> ch(/*capacity=*/4, /*spin_count=*/0);
|
||||||
|
|
||||||
|
std::thread producer([&] {
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
for (;;) {
|
||||||
|
try { ch.push(i); break; }
|
||||||
|
catch (const ChannelOverflowError&) { std::this_thread::yield(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ch.push_sentinel(SENTINEL);
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<bool> seen(N, false);
|
||||||
|
int values = 0;
|
||||||
|
int sentinels = 0;
|
||||||
|
bool duplicate = false;
|
||||||
|
int v;
|
||||||
|
while (values < N || sentinels == 0) {
|
||||||
|
if (!ch.try_pop_now(v)) { std::this_thread::yield(); continue; }
|
||||||
|
if (v == SENTINEL) { ++sentinels; continue; }
|
||||||
|
if (seen[v]) duplicate = true; else seen[v] = true;
|
||||||
|
++values;
|
||||||
|
}
|
||||||
|
producer.join();
|
||||||
|
|
||||||
|
REQUIRE_FALSE(duplicate);
|
||||||
|
REQUIRE(values == N);
|
||||||
|
REQUIRE(sentinels == 1);
|
||||||
|
// Sentinel held no ring slot; once drained the channel is fully empty.
|
||||||
|
REQUIRE(ch.size() == 0);
|
||||||
|
REQUIRE(ch.approx_size() == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user