blob: 1325534977a7191312d43fcbaede825424d56c2d [file]
/** @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);
}