Skip to content

Commit f7b6688

Browse files
committed
Data race checker: Added recursion support
1 parent edb3ff7 commit f7b6688

File tree

5 files changed

+143
-12
lines changed

5 files changed

+143
-12
lines changed

include/scl/utils/CMakeLists.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ include(../../../cmake/icm_testing.cmake)
77
# Global include for icm based tests
88
include_directories("../../include")
99

10-
file(GLOB test_files LIST_DIRECTORIES false "${CMAKE_CURRENT_SOURCE_DIR}" *_test.cpp)
11-
message(test_file: ${test_files})
10+
file(GLOB test_files LIST_DIRECTORIES false "${CMAKE_CURRENT_SOURCE_DIR}" *.test.cpp)
11+
message(test_files: ${test_files})
1212

1313
foreach(test_file ${test_files})
1414
get_filename_component(name_without_extension "${test_file}" NAME_WE)
1515
icm_add_test(
1616
NAME ${name_without_extension}
1717
SOURCES ${test_file})
1818

19-
if(MATCHES ${test_file} fail_*)
19+
if(${test_file} MATCHES fail_*)
2020
set_tests_properties(${name_without_extension} PROPERTIES
2121
WILL_FAIL TRUE)
2222
endif()

include/scl/utils/data_race_checker.h

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
// A writer enters a function and there is an active writer
2424
// A writer enters a function and there is an active reader
2525
//
26+
// N.B. This checks for recursion but there is a false positive if:
27+
// - A write starts
28+
// - A read on a separate thread starts
29+
// - That read finishes
30+
// - The original write continues but a data race is flagged
31+
// Although there is technically no race, this is probably unintended behaviour
32+
//
2633
// Wait-free? (maybe the CMPX is only lock-free?)
2734

2835
namespace scl {
@@ -36,6 +43,7 @@ namespace scl {
3643
struct check_state
3744
{
3845
std::atomic<size_t> num_readers { 0 };
46+
std::atomic<std::thread::id> last_thread_id { std::thread::id() };
3947
std::atomic<bool> is_writing { false };
4048
};
4149

@@ -52,22 +60,37 @@ bool can_read (const check_state& state)
5260
//==========================================
5361
void read_started (check_state& state)
5462
{
63+
auto this_thread_id = std::this_thread::get_id();
64+
auto last_thread_id = state.last_thread_id.exchange (this_thread_id);
65+
5566
++state.num_readers; // must be first
5667

5768
if (state.is_writing)
58-
DATA_RACE_DETECTED
59-
// read during active write
69+
{
70+
if (last_thread_id != this_thread_id)
71+
{ DATA_RACE_DETECTED }
72+
// read during active write
73+
}
6074
}
6175

6276
void write_started (check_state& state)
6377
{
78+
auto this_thread_id = std::this_thread::get_id();
79+
auto last_thread_id = state.last_thread_id.exchange (this_thread_id);
80+
6481
if (state.is_writing.exchange (true)) // must be first
65-
DATA_RACE_DETECTED
66-
// write during active write
82+
{
83+
if (last_thread_id != this_thread_id)
84+
{ DATA_RACE_DETECTED }
85+
// write during active write
86+
}
6787

6888
if (state.num_readers > 0)
69-
DATA_RACE_DETECTED
70-
// write during active read
89+
{
90+
if (last_thread_id != this_thread_id)
91+
{ DATA_RACE_DETECTED }
92+
// write during active read
93+
}
7194
}
7295

7396
void read_ended (check_state& state)

include/scl/utils/data_race_checker.test.h

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,52 @@ class test_vector
3636
data.push_back(value);
3737
}
3838

39+
// Test delegating a write-write
40+
void push_back_2(const T& v1, const T& v2) {
41+
scl::scoped_check<scl::check_type::write> _ (scl::data_race_registry<>::get_state (this));
42+
push_back(v1);
43+
push_back(v2);
44+
}
45+
46+
// Test delegating a write-read
47+
T& push_back_and_return(const T& value) {
48+
scl::scoped_check<scl::check_type::write> _ (scl::data_race_registry<>::get_state (this));
49+
data.push_back(value);
50+
return data.back();
51+
}
52+
3953
void pop_back() {
4054
scl::scoped_check<scl::check_type::write> _ (scl::data_race_registry<>::get_state (this));
4155
data.pop_back();
4256
}
4357

58+
// Test delegating a write-read
59+
const T& push_back_return_old_back(const T& value) {
60+
scl::scoped_check<scl::check_type::write> _ (scl::data_race_registry<>::get_state (this));
61+
62+
auto& old_back = back();
63+
push_back(value);
64+
return old_back;
65+
}
66+
67+
T& back() {
68+
scl::scoped_check<scl::check_type::read> _ (scl::data_race_registry<>::get_state (this));
69+
return data.back();
70+
}
71+
72+
const T& back() const {
73+
scl::scoped_check<scl::check_type::read> _ (scl::data_race_registry<>::get_state (this));
74+
return data.back();
75+
}
76+
77+
// Test delegating a read-write
78+
// This wouldn't normally occur as you'd have a const function calling a non-const function
79+
const T& back_with_default_write() {
80+
scl::scoped_check<scl::check_type::read> _ (scl::data_race_registry<>::get_state (this));
81+
push_back(T());
82+
return data.back();
83+
}
84+
4485
T& operator[](size_t index) {
4586
scl::scoped_check<scl::check_type::read> _ (scl::data_race_registry<>::get_state (this));
4687
return data[index];
@@ -61,6 +102,13 @@ class test_vector
61102
return data.capacity();
62103
}
63104

105+
// Test delegating a read-read
106+
std::pair<size_t, size_t> size_and_capacity() const
107+
{
108+
scl::scoped_check<scl::check_type::read> _ (scl::data_race_registry<>::get_state (this));
109+
return { size(), capacity() };
110+
}
111+
64112
private:
65113
std::vector<T> data;
66114
};
@@ -74,7 +122,7 @@ inline void test_no_data_race()
74122
vec.push_back (c);
75123

76124
// Then only read from the vector - no data race
77-
std::vector<std::thread> threads;
125+
std::vector<std::thread> threads;
78126

79127
for (auto _ : std::ranges::iota_view (0, 3))
80128
threads.emplace_back([&]
@@ -123,3 +171,57 @@ inline void test_data_race()
123171
for (auto& t : threads)
124172
t.join();
125173
}
174+
175+
inline void test_no_data_race_read_read()
176+
{
177+
test_vector<size_t> vec;
178+
179+
// Fill the vector first
180+
for (auto c : std::ranges::iota_view (0uz, 1'000uz))
181+
vec.push_back (c);
182+
183+
// Then only read from the vector - no data race
184+
std::vector<std::thread> threads;
185+
186+
for (auto _ : std::ranges::iota_view (0, 3))
187+
threads.emplace_back([&]
188+
{
189+
for (auto _ : std::ranges::iota_view (0uz, 1'000'000uz))
190+
{
191+
volatile auto c = 0uz;
192+
193+
if (! vec.empty())
194+
c = vec[vec.size() - 1];
195+
196+
auto sz_cp = vec.size_and_capacity();
197+
c = sz_cp.first;
198+
c = sz_cp.second;
199+
}
200+
});
201+
202+
for (auto& t : threads)
203+
t.join();
204+
}
205+
206+
inline void test_no_data_race_read_write()
207+
{
208+
test_vector<size_t> vec;
209+
volatile auto c = 0uz;
210+
vec.push_back (auto (c));
211+
c = vec.back_with_default_write();
212+
}
213+
214+
inline void test_no_data_race_write_read()
215+
{
216+
test_vector<size_t> vec;
217+
volatile auto c = 0uz;
218+
vec.push_back(auto(c));
219+
c = vec.push_back_return_old_back(auto (c));
220+
}
221+
222+
inline void test_no_data_race_write_write()
223+
{
224+
test_vector<size_t> vec;
225+
volatile auto c = 0uz;
226+
vec.push_back_2(auto (c), auto (c));
227+
}

include/scl/utils/fail_data_race_checker.test.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44

55
// Use exit(1) as ctest seems to count a raised signal as a pass...
6-
#define DATA_RACE_DETECTED std::exit(1);
6+
#define DATA_RACE_DETECTED { std::println ("ERROR: data race detected"); std::exit(1); }
77
#include "data_race_checker.test.h"
88

99
int main()

include/scl/utils/pass_data_race_checker.test.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
// Created by David Rowland on 11/02/2025.
33
//
44

5-
#define DATA_RACE_DETECTED std::exit(1);
5+
#define DATA_RACE_DETECTED { std::println ("ERROR: data race detected"); std::exit(1); }
66
#include "data_race_checker.test.h"
77

88
int main()
99
{
1010
test_no_data_race();
11+
12+
// Single threaded-tests
13+
test_no_data_race_read_read();
14+
test_no_data_race_read_write();
15+
test_no_data_race_write_read();
16+
test_no_data_race_write_write();
1117
}

0 commit comments

Comments
 (0)