packages feed

folly-clib-20250713.1537: folly/folly/coro/SharedMutex.h

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed 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.
 */

#pragma once

#include <atomic>
#include <cassert>
#include <limits>
#include <mutex>
#include <utility>

#include <folly/Executor.h>
#include <folly/SpinLock.h>
#include <folly/Synchronized.h>
#include <folly/coro/Coroutine.h>
#include <folly/coro/SharedLock.h>
#include <folly/coro/ViaIfAsync.h>

#if FOLLY_HAS_COROUTINES

namespace folly {
namespace coro {

/// The folly::coro::SharedMutexFair class provides a thread synchronisation
/// primitive that allows a coroutine to asynchronously acquire a lock on the
/// mutex.
///
/// The mutex supports three kinds of locks:
/// - exclusive-lock - Also known as a write-lock.
///                    While an exclusive lock is held, no other thread will be
///                    able to acquire either an exclusive lock or a shared
///                    lock until the exclusive lock is released.
/// - shared-lock    - Also known as a read-lock.
///                    The mutex permits multiple shared locks to be held
///                    concurrently but does not permit shared locks to be held
///                    concurrently with exclusive locks.
/// - upgrade-lock   - When an upgrade lock is held, others can still acquire
///                    shared locks but no exclusive lock, or upgrade lock.
///                    An upgrade lock can be later upgraded to an exclusive
///                    lock atomically after all the outstanding shared locks
///                    are released.
///
/// This mutex employs a fair lock acquisition strategy that attempts to process
/// locks in a mostly FIFO order in which they arrive at the mutex.
/// This means that if the mutex is currently read-locked and some coroutine
/// tries to acquire a write-lock, that subsequent read-lock attempts will
/// be queued up behind the write-lock, allowing the write-lock to be acquired
/// in a bounded amount of time.
///
/// One implication of this strategy is that it is not safe to unconditionally
/// acquire a new read-lock while already holding a read-lock, since it's
/// possible that this could lead to deadlock if there was another coroutine
/// that was currently waiting on a write-lock.
///
/// Notably, lock transition (e.g. upgrade an upgrade lock to an exclusive lock)
/// does not respect the FIFO order and is eager. This means a pending lock
/// transition will be processed as soon as possible. This is to avoid deadlock
/// in following scenario
/// 1. coroutine A has the upgrade lock
/// 2. coroutine B is waiting for an exclusive lock
/// 3. coroutine A tries to upgrade the lock to exclusive
/// Coroutine A and B would deadlock if we process the lock transition
/// (operation #3) in FIFO order. The readers will not be starved because they
/// are not blocked by the upgrade state to begin with. The writers/upgraders
/// will not be starved because they cannot acquire the lock anyway.
///
/// The locks acquired by this mutex do not have thread affinity. A coroutine
/// can acquire the lock on one thread and release the lock on another thread.
///
/// Example usage:
///
///  class AsyncStringSet {
///    mutable folly::coro::SharedMutexFair mutex_;
///    std::unordered_set<std::string> values_;
///
///    AsyncStringSet() = default;
///
///    folly::coro::Task<bool> insert(std::string value) {
///      auto lock = co_await mutex_.co_scoped_lock();
///      co_return values_.insert(value).second;
///    }
///
///    folly::coro::Task<bool> remove(std::string value) {
///      auto lock = co_await mutex_.co_scoped_lock();
///      co_return values_.erase(value) > 0;
///    }
///
///    folly::coro::Task<bool> contains(std::string value) const {
///      auto lock = co_await mutex_.co_scoped_lock_shared();
///      co_return values_.count(value) > 0;
///    }
///  };
class SharedMutexFair : private folly::NonCopyableNonMovable {
  template <typename Awaiter>
  class LockOperation;
  class LockAwaiter;
  class ScopedLockAwaiter;
  class LockSharedAwaiter;
  class ScopedLockSharedAwaiter;
  class LockUpgradeAwaiter;
  class ScopedLockUpgradeAwaiter;
  class UnlockUpgradeAndLockAwaiter;
  class ScopedUnlockUpgradeAndLockAwaiter;

 public:
  SharedMutexFair() noexcept = default;

  ~SharedMutexFair();

  /// Try to acquire an exclusive lock on the mutex synchronously.
  ///
  /// If this returns true then the exclusive lock was acquired synchronously
  /// and the caller is responsible for calling .unlock() later to release
  /// the exclusive lock. If this returns false then the lock was not acquired.
  ///
  /// Consider using a std::unique_lock to ensure the lock is released at the
  /// end of a scope.
  bool try_lock() noexcept;

  /// Try to acquire a shared lock on the mutex synchronously.
  ///
  /// If this returns true then the shared lock was acquired synchronously
  /// and the caller is responsible for calling .unlock_shared() later to
  /// release the shared lock.
  bool try_lock_shared() noexcept;

  /// Try to acquire an upgrade lock on the mutex synchronously.
  ///
  /// If this returns true then the upgrade lock was acquired synchronously
  /// and the caller is responsible for calling .unlock_upgrade() later to
  /// release the upgrade lock.
  bool try_lock_upgrade() noexcept;

  /// Asynchronously acquire an exclusive lock on the mutex.
  ///
  /// Returns a SemiAwaitable<void> type that requires the caller to inject
  /// an executor by calling .viaIfAsync(executor) and then co_awaiting the
  /// result to wait for the lock to be acquired. Note that if the caller is
  /// awaiting the lock operation within a folly::coro::Task then the current
  /// executor will be injected implicitly without needing to call
  /// .viaIfAsync().
  ///
  /// If the lock was acquired synchronously then the awaiting coroutine
  /// continues on the current thread without suspending.
  /// If the lock could not be acquired synchronously then the awaiting
  /// coroutine is suspended and later resumed on the specified executor when
  /// the lock becomes available.
  ///
  /// After this operation completes, the caller is responsible for calling
  /// .unlock() to release the lock.
  [[nodiscard]] LockOperation<LockAwaiter> co_lock() noexcept;

  /// Asynchronously acquire an exclusive lock on the mutex and return an object
  /// that will release the lock when it goes out of scope.
  ///
  /// Returns a SemiAwaitable<std::unique_lock<SharedMutexFair>> that, once
  /// associated with an executor using .viaIfAsync(), must be co_awaited to
  /// wait for the lock to be acquired.
  ///
  /// If the lock could be acquired immediately then the coroutine continues
  /// execution without suspending. Otherwise, the coroutine is suspended and
  /// will later be resumed on the specified executor once the lock has been
  /// acquired.
  [[nodiscard]] LockOperation<ScopedLockAwaiter> co_scoped_lock() noexcept;

  /// Asynchronously acquire a shared lock on the mutex.
  ///
  /// Returns a SemiAwaitable<void> type that requires the caller to inject
  /// an executor by calling .viaIfAsync(executor) and then co_awaiting the
  /// result to wait for the lock to be acquired. Note that if the caller is
  /// awaiting the lock operation within a folly::coro::Task then the current
  /// executor will be injected implicitly without needing to call
  /// .viaIfAsync().
  ///
  /// If the lock was acquired synchronously then the awaiting coroutine
  /// continues on the current thread without suspending.
  /// If the lock could not be acquired synchronously then the awaiting
  /// coroutine is suspended and later resumed on the specified executor when
  /// the lock becomes available.
  ///
  /// After this operation completes, the caller is responsible for calling
  /// .unlock_shared() to release the lock.
  [[nodiscard]] LockOperation<LockSharedAwaiter> co_lock_shared() noexcept;

  /// Asynchronously acquire a shared lock on the mutex and return an object
  /// that will release the lock when it goes out of scope.
  ///
  /// Returns a SemiAwaitable<std::shared_lock<SharedMutexFair>> that, once
  /// associated with an executor using .viaIfAsync(), must be co_awaited to
  /// wait for the lock to be acquired.
  ///
  /// If the lock could be acquired immediately then the coroutine continues
  /// execution without suspending. Otherwise, the coroutine is suspended and
  /// will later be resumed on the specified executor once the lock has been
  /// acquired.
  [[nodiscard]] LockOperation<ScopedLockSharedAwaiter>
  co_scoped_lock_shared() noexcept;

  /// Asynchronously acquire an upgrade lock on the mutex.
  ///
  /// Returns a SemiAwaitable<void> type that requires the caller to inject
  /// an executor by calling .viaIfAsync(executor) and then co_awaiting the
  /// result to wait for the lock to be acquired. Note that if the caller is
  /// awaiting the lock operation within a folly::coro::Task then the current
  /// executor will be injected implicitly without needing to call
  /// .viaIfAsync().
  ///
  /// If the lock was acquired synchronously then the awaiting coroutine
  /// continues on the current thread without suspending.
  /// If the lock could not be acquired synchronously then the awaiting
  /// coroutine is suspended and later resumed on the specified executor when
  /// the lock becomes available.
  ///
  /// After this operation completes, the caller is responsible for calling
  /// .unlock_upgrade() to release the lock.
  [[nodiscard]] LockOperation<LockUpgradeAwaiter> co_lock_upgrade() noexcept;

  /// Asynchronously acquire an upgrade lock on the mutex and return an object
  /// that will release the lock when it goes out of scope.
  ///
  /// Returns a SemiAwaitable<UpgradeLock<SharedMutexFair>> that, once
  /// associated with an executor using .viaIfAsync(), must be co_awaited to
  /// wait for the lock to be acquired.
  ///
  /// If the lock could be acquired immediately then the coroutine continues
  /// execution without suspending. Otherwise, the coroutine is suspended and
  /// will later be resumed on the specified executor once the lock has been
  /// acquired.
  [[nodiscard]] LockOperation<ScopedLockUpgradeAwaiter>
  co_scoped_lock_upgrade() noexcept;

  /// Asynchronously transition the currently held upgrade lock to exclusive.
  ///
  /// Returns a SemiAwaitable<void> type that requires the caller to inject
  /// an executor by calling .viaIfAsync(executor) and then co_awaiting the
  /// result to wait for the lock to be acquired. Note that if the caller is
  /// awaiting the lock operation within a folly::coro::Task then the current
  /// executor will be injected implicitly without needing to call
  /// .viaIfAsync().
  ///
  /// If the lock was transitioned synchronously then the awaiting coroutine
  /// continues on the current thread without suspending.
  /// If the lock could not be transitioned synchronously then the awaiting
  /// coroutine is suspended and later resumed on the specified executor when
  /// the lock becomes available.
  ///
  /// After this operation completes, the caller is responsible for calling
  /// .unlock() to release the lock.
  [[nodiscard]] LockOperation<UnlockUpgradeAndLockAwaiter>
  co_unlock_upgrade_and_lock() noexcept;

  /// Asynchronously transfer the currently held upgrade lock to exclusive
  /// and return an object that will release the exclusive lock when it
  /// goes out of scope.
  ///
  /// Notice that if the upgrade lock is acquired using
  /// `co_scoped_lock_upgrade()`, one should transfer the lock via
  /// `co_transition_lock(coro::UpgradeLock<coro::SharedMutex>&)` to avoid
  /// double unlock. This method is mostly useful if the original upgrade
  /// lock is acquired manually via `co_await mutex.co_lock_upgrade();`.
  ///
  /// Returns a SemiAwaitable<std::unique_lock<SharedMutexFair>> that, once
  /// associated with an executor using .viaIfAsync(), must be co_awaited to
  /// wait for the lock to be acquired.
  ///
  /// If the lock could be acquired immediately then the coroutine continues
  /// execution without suspending. Otherwise, the coroutine is suspended and
  /// will later be resumed on the specified executor once the lock has been
  /// acquired.
  [[nodiscard]] LockOperation<ScopedUnlockUpgradeAndLockAwaiter>
  co_scoped_unlock_upgrade_and_lock() noexcept;

  /// Release the exclusive lock.
  ///
  /// This will resume the next coroutine(s) waiting to acquire the lock, if
  /// any.
  void unlock() noexcept;

  /// Release a shared lock.
  ///
  /// If this is the last shared lock then this will resume the next
  /// coroutine(s) waiting to acquire the lock, if any.
  void unlock_shared() noexcept;

  /// Release an upgrade lock.
  ///
  /// This will resume the next coroutine(s) waiting to acquire an exclusive
  /// lock or an upgrade lock, if any.
  void unlock_upgrade() noexcept;

  /// Try to atomically transition an upgrade lock to an exclusive lock
  /// synchronously.
  ///
  /// If this returns true then the lock was acquired synchronously
  /// and the caller is responsible for calling .unlock() later to
  /// release the lock. Otherwise, the caller remains responsible for calling
  /// .unlock_upgrade() later to release the upgrade lock.
  bool try_unlock_upgrade_and_lock() noexcept;

 private:
  using folly_coro_aware_mutex = std::true_type;

  enum class LockType : std::uint8_t { EXCLUSIVE, UPGRADE, SHARED };

  class LockAwaiterBase {
   protected:
    friend class SharedMutexFair;

    explicit LockAwaiterBase(SharedMutexFair& mutex, LockType lockType) noexcept
        : mutex_(&mutex), nextAwaiter_(nullptr), lockType_(lockType) {}

    void resume() noexcept { continuation_.resume(); }

    SharedMutexFair* mutex_;
    LockAwaiterBase* nextAwaiter_;
    coroutine_handle<> continuation_;
    LockType lockType_;
  };

  class LockAwaiter : public LockAwaiterBase {
   public:
    explicit LockAwaiter(SharedMutexFair& mutex) noexcept
        : LockAwaiterBase(mutex, LockType::EXCLUSIVE) {}

    bool await_ready() noexcept { return mutex_->try_lock(); }

    FOLLY_CORO_AWAIT_SUSPEND_NONTRIVIAL_ATTRIBUTES bool await_suspend(
        coroutine_handle<> continuation) noexcept {
      auto lock = mutex_->state_.lock();

      // Exclusive lock can only be acquired if it's currently unlocked.
      if (lock->lockedFlagAndReaderCount_ == kUnlocked) {
        lock->lockedFlagAndReaderCount_ = kExclusiveLockFlag;
        return false;
      }

      // Append to the end of the waiters queue.
      continuation_ = continuation;
      ++lock->waitingWriterCount_;
      *lock->waitersTailNext_ = this;
      lock->waitersTailNext_ = &nextAwaiter_;
      return true;
    }

    void await_resume() noexcept {}
  };

  class LockSharedAwaiter : public LockAwaiterBase {
   public:
    explicit LockSharedAwaiter(SharedMutexFair& mutex) noexcept
        : LockAwaiterBase(mutex, LockType::SHARED) {}

    bool await_ready() noexcept { return mutex_->try_lock_shared(); }

    FOLLY_CORO_AWAIT_SUSPEND_NONTRIVIAL_ATTRIBUTES bool await_suspend(
        coroutine_handle<> continuation) noexcept {
      auto lock = mutex_->state_.lock();

      if (canLockShared(*lock)) {
        lock->lockedFlagAndReaderCount_ += kSharedLockCountIncrement;
        // check for potential overflow
        assert(lock->lockedFlagAndReaderCount_ >= kSharedLockCountIncrement);
        return false;
      }

      // Lock not available immediately.
      // Queue up for later resumption.
      continuation_ = continuation;
      *lock->waitersTailNext_ = this;
      lock->waitersTailNext_ = &nextAwaiter_;
      return true;
    }

    void await_resume() noexcept {}
  };

  class LockUpgradeAwaiter : public LockAwaiterBase {
   public:
    explicit LockUpgradeAwaiter(SharedMutexFair& mutex) noexcept
        : LockAwaiterBase(mutex, LockType::UPGRADE) {}

    bool await_ready() noexcept { return mutex_->try_lock_upgrade(); }

    FOLLY_CORO_AWAIT_SUSPEND_NONTRIVIAL_ATTRIBUTES bool await_suspend(
        coroutine_handle<> continuation) noexcept {
      auto lock = mutex_->state_.lock();

      if (canLockUpgrade(*lock)) {
        lock->lockedFlagAndReaderCount_ |= kUpgradeLockFlag;
        return false;
      }

      continuation_ = continuation;
      *lock->waitersTailNext_ = this;
      lock->waitersTailNext_ = &nextAwaiter_;
      return true;
    }

    void await_resume() noexcept {}
  };

  class UnlockUpgradeAndLockAwaiter : public LockAwaiterBase {
   public:
    explicit UnlockUpgradeAndLockAwaiter(SharedMutexFair& mutex) noexcept
        : LockAwaiterBase(mutex, LockType::EXCLUSIVE) {}

    bool await_ready() noexcept {
      return mutex_->try_unlock_upgrade_and_lock();
    }

    FOLLY_CORO_AWAIT_SUSPEND_NONTRIVIAL_ATTRIBUTES bool await_suspend(
        coroutine_handle<> continuation) noexcept {
      auto lock = mutex_->state_.lock();

      assert(lock->lockedFlagAndReaderCount_ & kUpgradeLockFlag);
      if (lock->lockedFlagAndReaderCount_ == kUpgradeLockFlag) {
        lock->lockedFlagAndReaderCount_ = kExclusiveLockFlag;
        return false;
      }

      continuation_ = continuation;
      assert(lock->upgrader_ == nullptr);
      lock->upgrader_ = this;
      return true;
    }

    void await_resume() noexcept {}
  };

  class ScopedLockAwaiter : public LockAwaiter {
   public:
    using LockAwaiter::LockAwaiter;

    [[nodiscard]] std::unique_lock<SharedMutexFair> await_resume() noexcept {
      LockAwaiter::await_resume();
      return std::unique_lock<SharedMutexFair>{*mutex_, std::adopt_lock};
    }
  };

  class ScopedLockSharedAwaiter : public LockSharedAwaiter {
   public:
    using LockSharedAwaiter::LockSharedAwaiter;

    [[nodiscard]] SharedLock<SharedMutexFair> await_resume() noexcept {
      LockSharedAwaiter::await_resume();
      return SharedLock<SharedMutexFair>{*mutex_, std::adopt_lock};
    }
  };

  class ScopedLockUpgradeAwaiter : public LockUpgradeAwaiter {
   public:
    using LockUpgradeAwaiter::LockUpgradeAwaiter;

    [[nodiscard]] UpgradeLock<SharedMutexFair> await_resume() noexcept {
      LockUpgradeAwaiter::await_resume();
      return UpgradeLock<SharedMutexFair>{*mutex_, std::adopt_lock};
    }
  };

  class ScopedUnlockUpgradeAndLockAwaiter : public UnlockUpgradeAndLockAwaiter {
   public:
    using UnlockUpgradeAndLockAwaiter::UnlockUpgradeAndLockAwaiter;

    [[nodiscard]] std::unique_lock<SharedMutexFair> await_resume() noexcept {
      UnlockUpgradeAndLockAwaiter::await_resume();
      return std::unique_lock<SharedMutexFair>{*mutex_, std::adopt_lock};
    }
  };

  friend class LockAwaiter;

  template <typename Awaiter>
  class LockOperation {
   public:
    explicit LockOperation(SharedMutexFair& mutex) noexcept : mutex_(mutex) {}

    auto viaIfAsync(folly::Executor::KeepAlive<> executor) const {
      return folly::coro::co_viaIfAsync(std::move(executor), Awaiter{mutex_});
    }

   private:
    SharedMutexFair& mutex_;
  };

  // There is an invariant that if the mutex state is unlocked, there must be no
  // waiters; the converse is obviously not always true. This is guaranteed by
  // the `getWaitersToResume` function. If there are waiters after an unlock_*
  // operation, the mutex state will transition to a non-unlocked state.
  // This helps avoid a redundant check on the waiters list when the mutex is
  // unlocked.
  struct State {
    State() noexcept
        : lockedFlagAndReaderCount_(kUnlocked),
          waitingWriterCount_(0),
          waitersHead_(nullptr),
          upgrader_(nullptr),
          waitersTailNext_(&waitersHead_) {}

    // bit 0 - exclusive lock is held
    // bit 1 - upgrade lock is held
    // bits 2-[31/63] - count of held shared locks
    std::size_t lockedFlagAndReaderCount_;
    std::size_t waitingWriterCount_;
    LockAwaiterBase* waitersHead_;
    // active upgrade lock holder who's waiting to upgrade to exclusive
    // at most one waiter can be in such state
    LockAwaiterBase* upgrader_;
    LockAwaiterBase** waitersTailNext_;
  };

  static LockAwaiterBase* getWaitersToResume(
      State& state, LockType prevLockType) noexcept;
  static LockAwaiterBase* scanReadersAndUpgrader(
      LockAwaiterBase* head,
      State& lockedState,
      LockType prevLockType) noexcept;

  static void resumeWaiters(LockAwaiterBase* awaiters) noexcept;
  static bool canLockShared(const State& state) noexcept {
    // a shared lock can be acquired if there are no exclusive locks held,
    // exclusive lock pending or lock transition pending
    // an exclusive lock is pending if there are queued waiters for
    // it; there is a pending lock transition if there is active upgrade lock
    // waiting to be upgraded to exclusive
    return state.lockedFlagAndReaderCount_ == kUnlocked ||
        (state.lockedFlagAndReaderCount_ != kExclusiveLockFlag &&
         state.waitingWriterCount_ == 0 && state.upgrader_ == nullptr);
  }
  static bool canLockUpgrade(const State& state) noexcept {
    return state.lockedFlagAndReaderCount_ == kUnlocked ||
        ((state.lockedFlagAndReaderCount_ &
          (kExclusiveLockFlag | kUpgradeLockFlag)) == 0 &&
         state.waitingWriterCount_ == 0);
  }

  static constexpr std::size_t kUnlocked = 0;
  static constexpr std::size_t kExclusiveLockFlag = 1;
  static constexpr std::size_t kUpgradeLockFlag = 2;
  static constexpr std::size_t kSharedLockCountIncrement = 4;

  folly::Synchronized<State, folly::SpinLock> state_;
};

inline SharedMutexFair::LockOperation<SharedMutexFair::LockAwaiter>
SharedMutexFair::co_lock() noexcept {
  return LockOperation<LockAwaiter>{*this};
}

inline SharedMutexFair::LockOperation<SharedMutexFair::LockSharedAwaiter>
SharedMutexFair::co_lock_shared() noexcept {
  return LockOperation<LockSharedAwaiter>{*this};
}

inline SharedMutexFair::LockOperation<SharedMutexFair::ScopedLockAwaiter>
SharedMutexFair::co_scoped_lock() noexcept {
  return LockOperation<ScopedLockAwaiter>{*this};
}

inline SharedMutexFair::LockOperation<SharedMutexFair::ScopedLockSharedAwaiter>
SharedMutexFair::co_scoped_lock_shared() noexcept {
  return LockOperation<ScopedLockSharedAwaiter>{*this};
}

inline SharedMutexFair::LockOperation<SharedMutexFair::LockUpgradeAwaiter>
SharedMutexFair::co_lock_upgrade() noexcept {
  return LockOperation<LockUpgradeAwaiter>{*this};
}

inline SharedMutexFair::LockOperation<SharedMutexFair::ScopedLockUpgradeAwaiter>
SharedMutexFair::co_scoped_lock_upgrade() noexcept {
  return LockOperation<ScopedLockUpgradeAwaiter>{*this};
}

inline SharedMutexFair::LockOperation<
    SharedMutexFair::UnlockUpgradeAndLockAwaiter>
SharedMutexFair::co_unlock_upgrade_and_lock() noexcept {
  return LockOperation<UnlockUpgradeAndLockAwaiter>{*this};
}

inline SharedMutexFair::LockOperation<
    SharedMutexFair::ScopedUnlockUpgradeAndLockAwaiter>
SharedMutexFair::co_scoped_unlock_upgrade_and_lock() noexcept {
  return LockOperation<ScopedUnlockUpgradeAndLockAwaiter>{*this};
}

// The default SharedMutex is SharedMutexFair.
using SharedMutex = SharedMutexFair;

} // namespace coro
} // namespace folly

#endif // FOLLY_HAS_COROUTINES