Skip to content
Merged
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
10 changes: 8 additions & 2 deletions docs/src/headers/mutex/shared_mutex.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,26 @@ struct shared_mutex {
void unlock(); // (3)

void unlock_shared(); // (4)

void downgrade(); // (5)
};
```

1. Asynchronously acquire the mutex in exclusive mode.
1. Asynchronously acquire the mutex in shared mode.
2. Asynchronously acquire the mutex in shared mode.
3. Release the mutex (mutex must be in exclusive mode).
3. Release the mutex (mutex must be in shared mode).
4. Release the mutex (mutex must be in shared mode).
5. Convert an exclusive lock into a shared one (mutex must be in exclusive mode).
Ownership of the mutex is never given up, hence no other exclusive owner can acquire the mutex in between.
Since the lock is shared after this call, it must be released by `unlock_shared()`.

### Return values
1. This method returns a sender of unspecified type. The sender does not return
any value, and completes once the mutex is acquired.
2. Same as (1).
3. This method doesn't return any value.
4. Same as (3).
5. Same as (3).

## Examples

Expand Down
84 changes: 68 additions & 16 deletions include/async/mutex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ namespace detail {
bool exclusive;
};

using node_list = frg::intrusive_list<
node,
frg::locate_member<
node,
frg::default_list_hook<node>,
&node::hook
>
>;

public:
shared_mutex() = default;

Expand Down Expand Up @@ -464,14 +473,7 @@ namespace detail {
// Only the owner ever transitions out of state::locked so we must be in state::contended.
assert(st.c == contention::contended);

frg::intrusive_list<
node,
frg::locate_member<
node,
frg::default_list_hook<node>,
&node::hook
>
> pending;
node_list pending;
{
frg::unique_lock lock(mutex_);

Expand Down Expand Up @@ -593,6 +595,63 @@ namespace detail {
next->complete();
}

// Atomically converts an exclusive lock into a shared one.
void downgrade() {
auto st = st_.load(std::memory_order_relaxed);
assert(st.c != contention::none);
assert(!st.shared_cnt);

// If there is no contention, we can downgrade without taking mutex_.
if (st.c == contention::locked) {
bool success = st_.compare_exchange_strong(
st,
state{.c = contention::locked, .shared_cnt = 1},
std::memory_order_release,
std::memory_order_relaxed
);
if (success)
return;
assert(!st.shared_cnt);
}
// Only the owner ever transitions out of state::locked so we must be in state::contended.
assert(st.c == contention::contended);

// Shared waiters can share the lock with us, hence we need to wake them.
// This also maintains the invariant that the first waiter is exclusive
// whenever the shared count is non-zero.
node_list pending;
{
frg::unique_lock lock(mutex_);

// Otherwise, we would not be in state::contended.
assert(!waiters_.empty());

// We keep holding the lock in shared mode.
unsigned int n = 1;
while (!waiters_.empty() && !waiters_.front()->exclusive) {
pending.push_back(waiters_.pop_front());
++n;
}
if (waiters_.empty()) {
// Release since state::locked with a non-zero shared count can be joined
// by try_lock_shared() without taking mutex_.
st_.store(
state{.c = contention::locked, .shared_cnt = n},
std::memory_order_release
);
} else {
// Hand-off to a waiter does not require a fence.
st_.store(
state{.c = contention::contended, .shared_cnt = n},
std::memory_order_relaxed
);
}
}

while (!pending.empty())
pending.pop_front()->complete();
}

private:
platform::mutex mutex_;

Expand All @@ -603,14 +662,7 @@ namespace detail {
// which can happen outside of mutex_.
std::atomic<state> st_{state{.c = contention::none, .shared_cnt = 0}};

frg::intrusive_list<
node,
frg::locate_member<
node,
frg::default_list_hook<node>,
&node::hook
>
> waiters_;
node_list waiters_;
};
}

Expand Down
76 changes: 76 additions & 0 deletions tests/mutex.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
#include <new>
#include <vector>

#include <async/basic.hpp>
#include <async/mutex.hpp>
#include <async/result.hpp>
#include <gtest/gtest.h>

namespace {

async::detached lockAndRecord(async::shared_mutex *mtx, std::vector<int> *order, int id) {
co_await mtx->async_lock();
order->push_back(id);
}

async::detached lockSharedAndRecord(async::shared_mutex *mtx, std::vector<int> *order, int id) {
co_await mtx->async_lock_shared();
order->push_back(id);
}

} // anonymous namespace

TEST(Mutex, TryLock) {
async::mutex m;

Expand All @@ -13,3 +29,63 @@ TEST(Mutex, TryLock) {
ASSERT_TRUE(m.try_lock());
m.unlock();
}

TEST(SharedMutex, Downgrade) {
async::shared_mutex mtx;

ASSERT_TRUE(mtx.try_lock());
mtx.downgrade();

// The mutex is now held in shared mode.
ASSERT_FALSE(mtx.try_lock());
ASSERT_TRUE(mtx.try_lock_shared());
mtx.unlock_shared();

mtx.unlock_shared();
ASSERT_TRUE(mtx.try_lock());
mtx.unlock();
}

TEST(SharedMutex, DowngradeWakesSharedWaiters) {
async::shared_mutex mtx;
std::vector<int> order;

ASSERT_TRUE(mtx.try_lock());
lockSharedAndRecord(&mtx, &order, 1);
lockSharedAndRecord(&mtx, &order, 2);
lockAndRecord(&mtx, &order, 3);
ASSERT_TRUE(order.empty());

// Both shared waiters can share the lock with us.
mtx.downgrade();
ASSERT_EQ(order, (std::vector<int>{1, 2}));

// The exclusive waiter has to wait for all three shared owners.
mtx.unlock_shared();
mtx.unlock_shared();
ASSERT_EQ(order, (std::vector<int>{1, 2}));
mtx.unlock_shared();
ASSERT_EQ(order, (std::vector<int>{1, 2, 3}));
mtx.unlock();
}

TEST(SharedMutex, DowngradeBehindExclusiveWaiter) {
async::shared_mutex mtx;
std::vector<int> order;

ASSERT_TRUE(mtx.try_lock());
lockAndRecord(&mtx, &order, 1);
lockSharedAndRecord(&mtx, &order, 2);

// The shared waiter must not overtake the exclusive waiter,
// hence downgrading does not wake any waiter here.
mtx.downgrade();
ASSERT_TRUE(order.empty());
ASSERT_FALSE(mtx.try_lock_shared());

mtx.unlock_shared();
ASSERT_EQ(order, (std::vector<int>{1}));
mtx.unlock();
ASSERT_EQ(order, (std::vector<int>{1, 2}));
mtx.unlock_shared();
}
Loading