| /** @file |
| |
| End-to-end test of the Clang thread-safety annotation chain. |
| |
| This translation unit is compiled with -Wthread-safety -Werror=thread-safety |
| under Clang (see CMakeLists.txt), so it doubles as a compile-time assertion: |
| the annotated example below must analyze cleanly, and any access to the |
| protected data outside a held guard would fail the build. |
| |
| The chain demonstrated here is: |
| * ts::mutex / ts::shared_mutex are capabilities (TsMutex.h, |
| TsSharedMutex.h), |
| * ts::lock_guard / ts::write_guard / |
| ts::read_guard supply the held capability for |
| the duration of a scope (TsMutex.h, |
| TsSharedMutex.h), |
| * the data members below are annotated as protected by that capability. |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include "tsutil/TsMutex.h" |
| #include "tsutil/TsSharedMutex.h" |
| |
| #include <catch2/catch_test_macros.hpp> |
| |
| #include <map> |
| #include <string> |
| #include <thread> |
| #include <vector> |
| |
| namespace |
| { |
| class ProtectedTable |
| { |
| public: |
| void |
| put(const std::string &key, int value) |
| { |
| ts::write_guard lock(_mutex); |
| _map[key] = value; // write under exclusive lock |
| bump_locked(); // calls a TS_REQUIRES helper while the lock is held |
| } |
| |
| int |
| get(const std::string &key) |
| { |
| ts::read_guard lock(_mutex); |
| auto it = _map.find(key); // read under shared lock |
| return it == _map.end() ? -1 : it->second; |
| } |
| |
| int |
| writes() const |
| { |
| ts::read_guard lock(_mutex); |
| return _writes; |
| } |
| |
| private: |
| // The compiler proves the caller holds _mutex; no runtime witness needed. |
| void |
| bump_locked() TS_REQUIRES(_mutex) |
| { |
| ++_writes; |
| } |
| |
| mutable ts::shared_mutex _mutex; |
| std::map<std::string, int> _map TS_GUARDED_BY(_mutex); |
| int _writes TS_GUARDED_BY(_mutex) = 0; |
| }; |
| |
| // The plain-mutex counterpart: ts::mutex guarded data taken through |
| // ts::lock_guard. |
| class ProtectedCounter |
| { |
| public: |
| void |
| add(int n) |
| { |
| ts::lock_guard lock(_mutex); |
| _sum += n; // write under exclusive lock |
| bump_locked(); // calls a TS_REQUIRES helper while the lock is held |
| } |
| |
| int |
| sum() const |
| { |
| ts::lock_guard lock(_mutex); |
| return _sum; |
| } |
| |
| int |
| adds() const |
| { |
| ts::lock_guard lock(_mutex); |
| return _adds; |
| } |
| |
| private: |
| void |
| bump_locked() TS_REQUIRES(_mutex) |
| { |
| ++_adds; |
| } |
| |
| mutable ts::mutex _mutex; |
| int _sum TS_GUARDED_BY(_mutex) = 0; |
| int _adds TS_GUARDED_BY(_mutex) = 0; |
| }; |
| |
| // Counterexample (kept as documentation): adding an unguarded accessor such as |
| // int peek() const { return _writes; } |
| // to ProtectedTable makes this file fail to compile under -Werror=thread-safety, |
| // with: "reading variable '_writes' requires holding shared_mutex '_mutex'". |
| // That compile-time failure is the whole point. |
| } // namespace |
| |
| TEST_CASE("ts::shared_mutex annotated guards provide mutual exclusion", "[thread_safety]") |
| { |
| ProtectedTable table; |
| |
| constexpr int n_threads = 8; |
| constexpr int per_thread = 1000; |
| |
| std::vector<std::thread> threads; |
| threads.reserve(n_threads); |
| for (int t = 0; t < n_threads; ++t) { |
| threads.emplace_back([&table, t]() { |
| const std::string key = "k" + std::to_string(t); |
| for (int i = 0; i < per_thread; ++i) { |
| table.put(key, i); |
| (void)table.get(key); |
| } |
| }); |
| } |
| for (auto &th : threads) { |
| th.join(); |
| } |
| |
| REQUIRE(table.writes() == n_threads * per_thread); |
| } |
| |
| TEST_CASE("ts::mutex annotated guard provides mutual exclusion", "[thread_safety]") |
| { |
| ProtectedCounter counter; |
| |
| constexpr int n_threads = 8; |
| constexpr int per_thread = 1000; |
| |
| std::vector<std::thread> threads; |
| threads.reserve(n_threads); |
| for (int t = 0; t < n_threads; ++t) { |
| threads.emplace_back([&counter]() { |
| for (int i = 0; i < per_thread; ++i) { |
| counter.add(1); |
| } |
| }); |
| } |
| for (auto &th : threads) { |
| th.join(); |
| } |
| |
| REQUIRE(counter.sum() == n_threads * per_thread); |
| REQUIRE(counter.adds() == n_threads * per_thread); |
| } |