Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bazelrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
test --test_output=errors

build --cxxopt='-std=c++20'
build --cxxopt='-std=c++23'
build --cxxopt='-Wno-self-move'
build --cxxopt='-fdiagnostics-color=always'

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/clang-tidy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@ jobs:
- name: Install the latest version of uv.
uses: astral-sh/setup-uv@v5

- name: Install clang-tidy-19
run: sudo apt-get install -y clang-tidy-19

- name: Configure CMake # We need `compile_commands.json` to be generated.
run: cmake --preset Release

- name: clang-tidy
- name: clang-tidy (v19)
run: |
uv run scripts/reduce_compile_commands.py build/compile_commands.json > compile_commands.json
PROJECT_SOURCE_FILES="$(python scripts/reduce_compile_commands.py build/compile_commands.json \
--names-only \
--exclude_dirs build exploration compile_checks benchmarks)"
echo $PROJECT_SOURCE_FILES \
| xargs clang-tidy -warnings-as-errors="*" -p compile_commands.json
| xargs clang-tidy-19 -warnings-as-errors="*" -p compile_commands.json
39 changes: 0 additions & 39 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,6 @@ jobs:
matrix:
configuration: ["Release", "Debug"]
settings:
- {
name: "Ubuntu GCC-11",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 11,
cc: "gcc-11",
cxx: "g++-11",
std: 20,
},
lib: "libstdc++11",
}
- {
name: "Ubuntu GCC-12",
os: ubuntu-24.04,
Expand Down Expand Up @@ -83,32 +70,6 @@ jobs:
},
lib: "libstdc++14",
}
- {
name: "Ubuntu Clang-17 + libc++",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 17,
cc: "clang-17",
cxx: "clang++-17",
std: 20,
},
lib: "libc++17",
}
- {
name: "Ubuntu Clang-18 + libc++",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
std: 20,
},
lib: "libc++18",
}
- {
name: "Ubuntu Clang-19 + libc++",
os: ubuntu-24.04,
Expand Down
17 changes: 16 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@
"streambuf": "cpp",
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp"
"typeinfo": "cpp",
"__bit_reference": "cpp",
"__hash_table": "cpp",
"__locale": "cpp",
"__node_handle": "cpp",
"__split_buffer": "cpp",
"__verbose_abort": "cpp",
"bitset": "cpp",
"complex": "cpp",
"execution": "cpp",
"forward_list": "cpp",
"ios": "cpp",
"locale": "cpp",
"print": "cpp",
"queue": "cpp",
"stack": "cpp"
}
}
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,32 @@ mutex_protected<int, std::shared_mutex> value;

### Locking multiple mutexes simultaneously

If you have multiple mutexes that you want to lock, you need to be careful to
avoid deadlocks that can happen if different threads lock them in a different
order. The solution is to lock them in the same order, which is best delegated
to `std::lock`, `std::try_lock` or `std::scoped_lock`, but those only work on
true mutexes. We want to return multiple guards, so supply `lock_protected` and
`try_lock_protected` for `mutex_protected` objects.

```cpp
mutex_protected<int> a(1);
mutex_protected<int> b(2);

auto [lb, la] = xyz::lock_protected(b, a);
*la += *lb;
```

```cpp
auto r = xyz::try_lock_protected(a, b);
if (r.has_value()) {
auto& [la, lb] = r.value();
*la += *lb;
} else {
assert(r.has_error());
// r.error() tells you which lock was contended.
}
```

## License

This code is licensed under the MIT License. See [LICENSE](LICENSE) for details.
Expand Down
4 changes: 2 additions & 2 deletions cmake/xyz_add_library.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ function(xyz_add_library)
endif()

if (NOT XYZ_VERSION)
set(XYZ_CXX_STANDARD cxx_std_20)
set(XYZ_CXX_STANDARD cxx_std_23)
else()
set(VALID_TARGET_VERSIONS 11 14 17 20 23)
set(VALID_TARGET_VERSIONS 23)
list(FIND VALID_TARGET_VERSIONS ${XYZ_VERSION} index)
if(index EQUAL -1)
message(FATAL_ERROR "TYPE must be one of <${VALID_TARGET_VERSIONS}>")
Expand Down
4 changes: 2 additions & 2 deletions cmake/xyz_add_object_library.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ function(xyz_add_object_library)
endif()

if (NOT XYZ_VERSION)
set(XYZ_CXX_STANDARD cxx_std_20)
set(XYZ_CXX_STANDARD cxx_std_23)
else()
set(VALID_TARGET_VERSIONS 11 14 17 20 23)
set(VALID_TARGET_VERSIONS 23)
list(FIND VALID_TARGET_VERSIONS ${XYZ_VERSION} index)
if(index EQUAL -1)
message(FATAL_ERROR "TYPE must be one of <${VALID_TARGET_VERSIONS}>")
Expand Down
4 changes: 2 additions & 2 deletions cmake/xyz_add_test.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ function(xyz_add_test)
message(FATAL_ERROR "NAME parameter must be supplied")
endif()
if (NOT XYZ_VERSION)
set(XYZ_VERSION 20)
set(XYZ_VERSION 23)
else()
set(VALID_TARGET_VERSIONS 11 14 17 20 23)
set(VALID_TARGET_VERSIONS 23)
list(FIND VALID_TARGET_VERSIONS ${XYZ_VERSION} index)
if(index EQUAL -1)
message(FATAL_ERROR "TYPE must be one of <${VALID_TARGET_VERSIONS}>")
Expand Down
23 changes: 21 additions & 2 deletions mutex_protected.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <chrono>
#include <concepts>
#include <expected>
#include <mutex>
#include <shared_mutex>
#include <thread>
Expand Down Expand Up @@ -244,14 +245,18 @@ class mutex_protected {
M mutex;
T v;

// Used by `xyz::lock_protected` when locking multiple mutex_protected
// objects.
// Used by `xyz::lock_protected` and `xyz::try_lock_protected` when locking
// multiple mutex_protected objects.
mutex_locked<T, std::unique_lock<M>> adopt_lock() {
return mutex_locked<T, std::unique_lock<M>>(&v, mutex, std::adopt_lock);
}

template <typename... MutexProtected>
friend auto lock_protected(MutexProtected &...mp);

template <typename... MutexProtected>
friend auto try_lock_protected(MutexProtected &...mps)
-> std::expected<std::tuple<decltype(mps.adopt_lock())...>, int>;
};

template <typename... MutexProtected>
Expand All @@ -260,6 +265,20 @@ auto lock_protected(MutexProtected &...mps) {
return std::make_tuple(mps.adopt_lock()...);
}

template <typename... MutexProtected>
auto try_lock_protected(MutexProtected &...mps)
-> std::expected<std::tuple<decltype(mps.adopt_lock())...>, int> {
int r = std::try_lock(mps.mutex...);
if (r >= 0) {
return std::expected<std::tuple<decltype(mps.adopt_lock())...>, int>(
std::unexpect, r);

} else {
return std::expected<std::tuple<decltype(mps.adopt_lock())...>, int>(
std::in_place, mps.adopt_lock()...);
}
}

} // namespace xyz

#endif // XYZ_MUTEX_PROTECTED_H
49 changes: 47 additions & 2 deletions mutex_protected_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,51 @@ TYPED_TEST(MutexProtectedTest, LockMultiple) {
}
}

TYPED_TEST(MutexProtectedTest, TryLockMultiple) {
mutex_protected<int, TypeParam> a(1);
mutex_protected<int, TypeParam> b(2);
{
auto r = xyz::try_lock_protected(a, b);
ASSERT_TRUE(r.has_value());

auto& [la, lb] = r.value();
EXPECT_EQ(*la, 1);
EXPECT_EQ(*lb, 2);
*la += 10;
*lb += 10;
}
{
auto r = xyz::try_lock_protected(a, b);
ASSERT_TRUE(r.has_value());
auto& [la, lb] = r.value();
EXPECT_EQ(*la, 11);
EXPECT_EQ(*lb, 12);
}
{
auto la = a.lock();
std::thread t([&]() { // Needed due to recursive locks.
auto r = xyz::try_lock_protected(a, b);
ASSERT_FALSE(r.has_value());
EXPECT_EQ(r.error(), 0);
});
t.join();
}
{
auto lb = b.lock();
std::thread t([&]() { // Needed due to recursive locks.
auto r = xyz::try_lock_protected(a, b);
ASSERT_FALSE(r.has_value());
EXPECT_EQ(r.error(), 1);
});
t.join();
}
{
auto [lb, la] = xyz::lock_protected(b, a);
EXPECT_EQ(*la, 11);
EXPECT_EQ(*lb, 12);
}
}

template <typename T>
class SharedMutexProtectedTest : public testing::Test {};

Expand Down Expand Up @@ -316,8 +361,8 @@ TYPED_TEST(SharedMutexProtectedTest, ThreadSafetyCorrectness) {
}
EXPECT_EQ(*value.lock(), writers * iters);

// Hopefully this stops things from being optimized away, but I don't see how
// this could fail.
// Hopefully this stops things from being optimized away, but I don't see
// how this could fail.
EXPECT_LE(*grand_total.lock(), (long long)readers * writers * iters * iters);
}

Expand Down
Loading