Skip to content

Commit 021e4c5

Browse files
committed
✨ Add test concurrency policy
Problem: - In order to find threading bugs more easily, TSan must have some non-determinism to work with. Solution: - Introduce the test concurrency policy, which adds random-duration sleeps on entering a critical section.
1 parent cea9d67 commit 021e4c5

File tree

3 files changed

+156
-1
lines changed

3 files changed

+156
-1
lines changed

include/conc/test.hpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#pragma once
2+
3+
#include <conc/concurrency.hpp>
4+
5+
#if __STDC_HOSTED__ == 0
6+
#error conc::test_policy is designed for desktop testing and requires a hosted implementation
7+
#endif
8+
9+
#include <algorithm>
10+
#include <array>
11+
#include <functional>
12+
#include <iterator>
13+
#include <mutex>
14+
#include <random>
15+
#include <thread>
16+
#include <utility>
17+
18+
namespace conc {
19+
class test_policy {
20+
template <typename> static inline std::mutex m{};
21+
22+
[[maybe_unused]] static auto get_rng() -> auto & {
23+
thread_local auto rng = [] {
24+
std::array<int, std::mt19937::state_size> seed_data;
25+
std::random_device r;
26+
std::generate_n(seed_data.data(), seed_data.size(), std::ref(r));
27+
std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
28+
return std::mt19937{seq};
29+
}();
30+
return rng;
31+
}
32+
33+
template <typename Uniq> struct [[nodiscard]] cs_raii_t {
34+
cs_raii_t() {
35+
m<Uniq>.lock();
36+
++lock_count;
37+
}
38+
~cs_raii_t() {
39+
++unlock_count;
40+
m<Uniq>.unlock();
41+
}
42+
};
43+
44+
public:
45+
static inline std::atomic<std::uint32_t> lock_count{};
46+
static inline std::atomic<std::uint32_t> unlock_count{};
47+
48+
static auto reset_counts() {
49+
lock_count = 0;
50+
unlock_count = 0;
51+
}
52+
53+
template <typename Uniq = void, std::invocable F, std::predicate... Pred>
54+
requires(sizeof...(Pred) < 2)
55+
static inline auto call_in_critical_section(F &&f, Pred &&...pred)
56+
-> decltype(std::forward<F>(f)()) {
57+
while (true) {
58+
std::uniform_int_distribution<> dis{5, 10};
59+
auto const d1 = std::chrono::milliseconds{dis(get_rng())};
60+
auto const d2 = std::chrono::milliseconds{dis(get_rng())};
61+
std::this_thread::sleep_for(d1);
62+
63+
[[maybe_unused]] cs_raii_t<Uniq> lock{};
64+
std::this_thread::sleep_for(d2);
65+
if ((... and pred())) {
66+
return std::forward<F>(f)();
67+
}
68+
}
69+
}
70+
};
71+
72+
template <> inline auto injected_policy<> = test_policy{};
73+
} // namespace conc

test/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20)
2222
add_tests(
2323
FILES
2424
conc_standard_policy
25+
conc_test_policy
2526
concepts
2627
freestanding_conc_injected_policy
2728
hosted_conc_injected_policy
2829
MULL_EXCLUSIONS
29-
conc_standard_policy)
30+
conc_standard_policy
31+
conc_test_policy)
3032

3133
add_compile_fail_test(fail_no_conc_policy.cpp LIBRARIES concurrency)
3234
endif()

test/conc_test_policy.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include <conc/test.hpp>
2+
3+
#include <catch2/catch_test_macros.hpp>
4+
5+
#include <algorithm>
6+
#include <array>
7+
#include <chrono>
8+
#include <functional>
9+
#include <iterator>
10+
#include <random>
11+
#include <thread>
12+
13+
TEST_CASE("test policy allows 'recursive' critical_sections", "[test_policy]") {
14+
auto const value = conc::call_in_critical_section(
15+
[] { return conc::call_in_critical_section([] { return 1; }); });
16+
CHECK(value == 1);
17+
}
18+
19+
namespace {
20+
struct rng_CS;
21+
struct count_CS;
22+
23+
auto get_rng() -> auto & {
24+
std::array<int, std::mt19937::state_size> seed_data;
25+
std::random_device r;
26+
std::generate_n(seed_data.data(), seed_data.size(), std::ref(r));
27+
std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
28+
static std::mt19937 rng(seq);
29+
return rng;
30+
}
31+
} // namespace
32+
33+
TEST_CASE("test policy works", "[test_policy]") {
34+
conc::test_policy::reset_counts();
35+
constexpr auto N = 10u;
36+
auto &rng = get_rng();
37+
auto count = 0;
38+
auto dis = std::uniform_int_distribution{1, 10};
39+
40+
std::array<std::thread, N> threads{};
41+
for (auto i = 0u; i < N; ++i) {
42+
threads[i] = std::thread{[&] {
43+
auto const d = conc::call_in_critical_section(
44+
[&] { return std::chrono::milliseconds{dis(rng)}; });
45+
std::this_thread::sleep_for(d);
46+
conc::call_in_critical_section([&] { ++count; });
47+
}};
48+
}
49+
for (auto i = 0u; i < N; ++i) {
50+
threads[i].join();
51+
}
52+
53+
CHECK(count == N);
54+
CHECK(conc::test_policy::lock_count == N * 2);
55+
CHECK(conc::test_policy::unlock_count == N * 2);
56+
}
57+
58+
TEST_CASE("test policy allows different-ID critical_sections",
59+
"[test_policy]") {
60+
auto &rng = get_rng();
61+
auto count = 0;
62+
auto dis = std::uniform_int_distribution{1, 10};
63+
64+
auto t1 = std::thread{[&] {
65+
auto const d = conc::call_in_critical_section<rng_CS>(
66+
[&] { return std::chrono::milliseconds{dis(rng)}; });
67+
std::this_thread::sleep_for(d);
68+
conc::call_in_critical_section<count_CS>([&] { ++count; });
69+
}};
70+
auto t2 = std::thread{[&] {
71+
auto const d = conc::call_in_critical_section<rng_CS>(
72+
[&] { return std::chrono::milliseconds{dis(rng)}; });
73+
std::this_thread::sleep_for(d);
74+
conc::call_in_critical_section<count_CS>([&] { ++count; });
75+
}};
76+
t1.join();
77+
t2.join();
78+
79+
CHECK(count == 2);
80+
}

0 commit comments

Comments
 (0)