add nf7::Mutex

This commit is contained in:
falsycat 2023-08-18 13:32:33 +09:00
parent 95b8f26b77
commit 2e0f089611
4 changed files with 262 additions and 0 deletions

View File

@ -7,6 +7,7 @@ target_link_libraries(nf7_iface
target_sources(nf7_iface
PRIVATE
common/exception.cc
common/mutex.cc
version.cc
PUBLIC
common/container.hh
@ -14,6 +15,7 @@ target_sources(nf7_iface
common/exception.hh
common/future.hh
common/leak_detector.hh
common/mutex.hh
common/observer.hh
common/task.hh
common/task_context.hh
@ -37,6 +39,7 @@ target_sources(nf7_iface_test
common/exception_test.cc
common/future_test.cc
common/leak_detector_test.cc
common/mutex_test.cc
common/observer_test.cc
common/observer_test.hh
common/task_test.cc

137
iface/common/mutex.cc Normal file
View File

@ -0,0 +1,137 @@
// No copyright
#include "iface/common/mutex.hh"
#include <cstdlib>
#include <deque>
#include <mutex>
#include <utility>
#include "iface/common/exception.hh"
namespace nf7 {
class Mutex::Impl final : public std::enable_shared_from_this<Impl> {
public:
Impl() = default;
Future<SharedToken> Lock() noexcept;
SharedToken TryLock();
void Unlock() noexcept;
void TearDown() noexcept;
private:
SharedToken MakeToken();
private:
mutable std::mutex mtx_;
std::weak_ptr<Token> current_;
std::deque<Future<SharedToken>::Completer> pends_;
};
class Mutex::Token final {
public:
Token() = delete;
explicit Token(const std::shared_ptr<Impl>& impl) noexcept
: impl_(impl) { }
~Token() noexcept {
if (auto impl = impl_.lock()) {
impl->Unlock();
}
}
Token(const Token&) = delete;
Token(Token&&) = delete;
Token& operator=(const Token&) = delete;
Token& operator=(Token&&) = delete;
private:
const std::weak_ptr<Impl> impl_;
};
Future<Mutex::SharedToken> Mutex::Impl::Lock() noexcept
try {
std::unique_lock<std::mutex> k {mtx_};
if (current_.lock()) {
pends_.emplace_back();
return pends_.back().future();
}
return Future<SharedToken> {MakeToken()};
} catch (const std::exception&) {
return Future<SharedToken> {
Exception::MakePtr("failed to queue lock request"),
};
}
Mutex::SharedToken Mutex::Impl::TryLock()
try {
std::unique_lock<std::mutex> k {mtx_};
return current_.lock()? nullptr: MakeToken();
} catch (const std::exception&) {
throw Exception {"failed to acquire lock"};
}
void Mutex::Impl::Unlock() noexcept
try {
std::unique_lock<std::mutex> k {mtx_};
current_ = {};
if (pends_.empty()) {
return;
}
auto comp = std::move(pends_.front());
pends_.pop_front();
try {
auto token = MakeToken();
k.unlock();
comp.Complete(std::move(token));
} catch (const std::bad_alloc&) {
k.unlock();
comp.Throw(Exception::MakePtr("failed to acquire lock"));
}
} catch (const std::system_error&) {
std::abort();
}
void Mutex::Impl::TearDown() noexcept
try {
std::unique_lock<std::mutex> k {mtx_};
pends_.clear();
} catch (const std::system_error&) {
std::abort();
}
Mutex::SharedToken Mutex::Impl::MakeToken()
try {
auto ret = std::make_shared<Token>(shared_from_this());
current_ = ret;
return ret;
} catch (const std::bad_alloc&) {
throw Exception {"failed to make new mutex token"};
}
Mutex::Mutex()
try : impl_(std::make_shared<Impl>()) {
} catch (const Exception&) {
throw Exception {"memory shortage"};
}
Mutex::~Mutex() noexcept {
impl_->TearDown();
}
Future<Mutex::SharedToken> Mutex::Lock() noexcept {
return impl_->Lock();
}
Mutex::SharedToken Mutex::TryLock() {
return impl_->TryLock();
}
} // namespace nf7

36
iface/common/mutex.hh Normal file
View File

@ -0,0 +1,36 @@
// No copyright
#pragma once
#include <memory>
#include "iface/common/future.hh"
namespace nf7 {
class Mutex final {
private:
class Impl;
public:
class Token;
using SharedToken = std::shared_ptr<Token>;
public:
Mutex();
~Mutex() noexcept;
Mutex(const Mutex&) = delete;
Mutex(Mutex&&) = delete;
Mutex& operator=(const Mutex&) = delete;
Mutex& operator=(Mutex&&) = delete;
public:
Future<SharedToken> Lock() noexcept;
SharedToken TryLock();
private:
std::shared_ptr<Impl> impl_;
};
} // namespace nf7

View File

@ -0,0 +1,86 @@
// No copyright
#include "iface/common/mutex.hh"
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <optional>
#include <vector>
using namespace std::literals;
TEST(Mutex, TryLock) {
nf7::Mutex mtx;
auto k = mtx.TryLock();
EXPECT_TRUE(k);
}
TEST(Mutex, TryLockFails) {
nf7::Mutex mtx;
auto k1 = mtx.TryLock();
auto k2 = mtx.TryLock();
EXPECT_FALSE(k2);
}
TEST(Mutex, Lock) {
nf7::Mutex mtx;
auto fu = mtx.Lock();
EXPECT_TRUE(fu.done());
}
TEST(Mutex, LockPending) {
nf7::Mutex mtx;
auto k = mtx.TryLock();
auto fu = mtx.Lock();
EXPECT_TRUE(fu.yet());
}
TEST(Mutex, LockWithDelay) {
nf7::Mutex mtx;
auto k = mtx.TryLock();
auto fu = mtx.Lock();
k = nullptr;
EXPECT_TRUE(fu.done());
}
TEST(Mutex, LockAbort) {
std::optional<nf7::Mutex> mtx;
mtx.emplace();
auto k = mtx->TryLock();
auto fu = mtx->Lock();
mtx = std::nullopt;
EXPECT_TRUE(fu.error());
}
TEST(Mutex, ChaoticLock) {
nf7::Mutex mtx;
std::atomic<bool> flag = false;
std::vector<std::thread> threads;
threads.resize(16);
for (auto& th : threads) {
th = std::thread {[&]() {
mtx.Lock()
.Then([&](auto&) {
const auto flag_at_first = flag.exchange(true);
EXPECT_FALSE(flag_at_first);
std::this_thread::sleep_for(10ms);
const auto flag_at_last = flag.exchange(false);
EXPECT_TRUE(flag_at_last);
})
.Catch([&](const auto& e) {
FAIL() << e;
});
}};
}
for (auto& th : threads) {
th.join();
}
}