Skip to content

Commit 2a8a8a7

Browse files
committed
Add configuration option to make assertions thread-safe
All the previous refactoring to make the assertion fast paths smaller and faster also allows us to implement the fast paths just with thread-local and atomic variables, without full mutexes. However, the performance overhead of thread-safe assertions is still significant for single threaded usage: | slowdown | Debug | Release | |-----------|--------:|--------:| | fast path | 1.04x | 1.43x | | slow path | 1.16x | 1.22x | Thus, we don't make the assertions thread-safe by default, and instead provide a build-time configuration option that the users can set to get thread-safe assertions. This commit is functional, but it still needs some follow-up work: * We do not need full seq_cst increments for the atomic counters, and using weaker ones can be faster. * We brute-force updating the reporter-friendly totals from internal atomic counters by doing it everywhere. We should properly trace where this is needed instead. * Message macros (`INFO`, `UNSCOPED_INFO`, `CAPTURE`, etc) are not made thread safe in this commit, but they can be made thread safe in the future, by building on top of this work. * Add more tests, including with thread-sanitizer, and compiled examples to the repository. Right now, these changes have been compiled with tsan manually, but these tests are not added to CI. Closes #2948
1 parent 900a6d5 commit 2a8a8a7

File tree

12 files changed

+332
-49
lines changed

12 files changed

+332
-49
lines changed

BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ expand_template(
7676
"#cmakedefine CATCH_CONFIG_WINDOWS_SEH": "",
7777
"#cmakedefine CATCH_CONFIG_USE_BUILTIN_CONSTANT_P": "",
7878
"#cmakedefine CATCH_CONFIG_NO_USE_BUILTIN_CONSTANT_P": "",
79+
"#cmakedefine CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS": "",
80+
"#cmakedefine CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS": "",
7981
},
8082
template = "src/catch2/catch_user_config.hpp.in",
8183
)

CMake/CatchConfigOptions.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ set(_OverridableOptions
4545
"EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT"
4646
"USE_BUILTIN_CONSTANT_P"
4747
"DEPRECATION_ANNOTATIONS"
48+
"EXPERIMENTAL_THREAD_SAFE_ASSERTIONS"
4849
)
4950

5051
foreach(OptionName ${_OverridableOptions})

docs/configuration.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
[Other toggles](#other-toggles)<br>
1515
[Enabling stringification](#enabling-stringification)<br>
1616
[Disabling exceptions](#disabling-exceptions)<br>
17+
[Disabling deprecation warnings](#disabling-deprecation-warnings)<br>
1718
[Overriding Catch's debug break (`-b`)](#overriding-catchs-debug-break--b)<br>
1819
[Static analysis support](#static-analysis-support)<br>
20+
[Experimental thread safety](#experimental-thread-safety)<br>
1921

2022
Catch2 is designed to "just work" as much as possible, and most of the
2123
configuration options below are changed automatically during compilation,
@@ -314,6 +316,21 @@ no backwards compatibility guarantees._
314316
are not meant to be runnable, only "scannable".
315317
316318
319+
## Experimental thread safety
320+
321+
> Introduced in Catch2 X.Y.Z
322+
323+
Catch2 can optionally support thread-safe assertions, that means, multiple
324+
user-spawned threads can use the assertion macros at the same time. Due
325+
to the performance cost this imposes even on single-threaded usage, Catch2
326+
defaults to non-thread-safe assertions.
327+
328+
CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS // enables thread safe assertions
329+
CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS // force-disables thread safe assertions
330+
331+
See [the documentation on thread safety in Catch2](thread-safety.md#top)
332+
for details on which macros are safe and other notes.
333+
317334
318335
---
319336

docs/test-fixtures.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
**Contents**<br>
55
[Non-Templated test fixtures](#non-templated-test-fixtures)<br>
66
[Templated test fixtures](#templated-test-fixtures)<br>
7-
[Signature-based parameterised test fixtures](#signature-based-parametrised-test-fixtures)<br>
7+
[Signature-based parameterised test fixtures](#signature-based-parameterised-test-fixtures)<br>
88
[Template fixtures with types specified in template type lists](#template-fixtures-with-types-specified-in-template-type-lists)<br>
99

1010
## Non-Templated test fixtures

docs/thread-safety.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<a id="top"></a>
2+
# Thread safety in Catch2
3+
4+
**Contents**<br>
5+
[Using assertion macros from multiple threads](#using-assertion-macros-from-multiple-threads)<br>
6+
[examples](#examples)<br>
7+
[`STATIC_REQUIRE` and `STATIC_CHECK`](#static_require-and-static_check)<br>
8+
[Fatal errors and multiple threads](#fatal-errors-and-multiple-threads)<br>
9+
[Performance overhead](#performance-overhead)<br>
10+
11+
> Thread safe assertions were introduced in Catch2 X.Y.Z
12+
13+
Thread safety in Catch2 is currently limited to all the assertion macros.
14+
Interacting with benchmark macros, message macros (e.g. `INFO` or `CAPTURE`),
15+
sections macros, generator macros, or test case macros is not thread-safe.
16+
The message macros are likely to be made thread-safe in the future, but
17+
the way sections define test runs is incompatible with user being able
18+
to spawn threads arbitrarily, thus that limitation is here to stay.
19+
20+
**Important: thread safety in Catch2 is [opt-in](configuration.md#experimental-thread-safety)**
21+
22+
23+
## Using assertion macros from multiple threads
24+
25+
The full set of Catch2's runtime assertion macros is thread-safe. However,
26+
it is important to keep in mind that their semantics might not support
27+
being used from user-spawned threads.
28+
29+
Specifically, the `REQUIRE` family of assertion macros have semantics
30+
of stopping the test execution on failure. This is done by throwing
31+
an exception, but since the user-spawned thread will not have the test-level
32+
try-catch block ready to catch the test failure exception, failing a
33+
`REQUIRE` assertion inside this thread will terminate the process.
34+
35+
The `CHECK` family of assertions does not have this issue, because it
36+
does not try to stop the test execution.
37+
38+
Note that `CHECKED_IF` and `CHECKED_ELSE` are also thread safe (internally
39+
they are assertion macro + an if).
40+
41+
**`SKIP()`, `FAIL()`, `SUCCEED()` are not assertion macros, and are not
42+
thread-safe.**
43+
44+
45+
## examples
46+
47+
### `REQUIRE` from main thread, `CHECK` from spawned threads
48+
49+
```cpp
50+
TEST_CASE( "Failed REQUIRE in main thread is fine" ) {
51+
std::vector<std::jthread> threads;
52+
for ( size_t t = 0; t < 16; ++t) {
53+
threads.emplace_back( []() {
54+
for (size_t i = 0; i < 10'000; ++i) {
55+
CHECK( true );
56+
CHECK( false );
57+
}
58+
} );
59+
}
60+
61+
REQUIRE( false );
62+
}
63+
```
64+
This will work as expected, that is, the process will finish running
65+
normally, the test case will fail and there will be the correct count of
66+
passing and failing assertions (160000 and 160001 respectively). However,
67+
it is important to understand that when the main thread fails its assertion,
68+
the spawned threads will keep running.
69+
70+
71+
### `REQUIRE` from spawned threads
72+
73+
```cpp
74+
TEST_CASE( "Successful REQUIRE in spawned thread is fine" ) {
75+
std::vector<std::jthread> threads;
76+
for ( size_t t = 0; t < 16; ++t) {
77+
threads.emplace_back( []() {
78+
for (size_t i = 0; i < 10'000; ++i) {
79+
REQUIRE( true );
80+
}
81+
} );
82+
}
83+
}
84+
```
85+
This will also work as expected, because the `REQUIRE` is successful.
86+
87+
```cpp
88+
TEST_CASE( "Failed REQUIRE in spawned thread is fine" ) {
89+
std::vector<std::jthread> threads;
90+
for ( size_t t = 0; t < 16; ++t) {
91+
threads.emplace_back( []() {
92+
for (size_t i = 0; i < 10'000; ++i) {
93+
REQUIRE( false );
94+
}
95+
} );
96+
}
97+
}
98+
```
99+
This will fail catastrophically and terminate the process.
100+
101+
102+
## `STATIC_REQUIRE` and `STATIC_CHECK`
103+
104+
None of `STATIC_REQUIRE`, `STATIC_REQUIRE_FALSE`, `STATIC_CHECK`, and
105+
`STATIC_CHECK_FALSE` are currently thread safe. This might be surprising
106+
given that they are a compile-time checks, but they also rely on the
107+
message macros to register the result with reporter at runtime.
108+
109+
110+
## Fatal errors and multiple threads
111+
112+
By default, Catch2 tries to catch fatal errors (POSIX signals/Windows
113+
Structured Exceptions) and report something useful to the user. This
114+
always happened on a best-effort basis, but in presence of multiple
115+
threads and locks the chance of it working decreases. If this starts
116+
being an issue for you, [you can disable it](configuration.md#other-toggles).
117+
118+
119+
## Performance overhead
120+
121+
In the worst case, which is optimized build and assertions using the
122+
fast path for successful assertions, the performance overhead of using
123+
the thread-safe assertion implementation can reach 40%. In other cases,
124+
the overhead will be smaller, between 4% and 20%.
125+
126+
127+
128+
---
129+
130+
[Home](Readme.md#top)

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ set(IMPL_HEADERS
139139
${SOURCES_DIR}/internal/catch_test_registry.hpp
140140
${SOURCES_DIR}/internal/catch_test_spec_parser.hpp
141141
${SOURCES_DIR}/internal/catch_textflow.hpp
142+
${SOURCES_DIR}/internal/catch_thread_support.hpp
142143
${SOURCES_DIR}/internal/catch_to_string.hpp
143144
${SOURCES_DIR}/internal/catch_uncaught_exceptions.hpp
144145
${SOURCES_DIR}/internal/catch_uniform_floating_point_distribution.hpp

src/catch2/catch_all.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
#include <catch2/internal/catch_test_registry.hpp>
122122
#include <catch2/internal/catch_test_spec_parser.hpp>
123123
#include <catch2/internal/catch_textflow.hpp>
124+
#include <catch2/internal/catch_thread_support.hpp>
124125
#include <catch2/internal/catch_to_string.hpp>
125126
#include <catch2/internal/catch_uncaught_exceptions.hpp>
126127
#include <catch2/internal/catch_uniform_floating_point_distribution.hpp>

src/catch2/catch_user_config.hpp.in

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@
196196
#endif
197197

198198

199+
#cmakedefine CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS
200+
#cmakedefine CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS
201+
202+
#if defined( CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS ) && \
203+
defined( CATCH_CONFIG_NO_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS )
204+
# error Cannot force EXPERIMENTAL_THREAD_SAFE_ASSERTIONS to both ON and OFF
205+
#endif
206+
199207

200208
// ------
201209
// Simple toggle defines

0 commit comments

Comments
 (0)