void release();
};
+ class ExclusionOwner;
+ class ExclusionState;
+
class ExclusionLock {
- shared_ptr<Task> m_owner;
- ExclusionLock(shared_ptr<Task> owner);
+ shared_ptr<ExclusionOwner> m_owner;
+ ExclusionLock(shared_ptr<ExclusionOwner> owner);
friend class Exclusion;
public:
/// Explicit default constructor because we have other kinds
};
class Exclusion {
- mutex m_mutex;
- weak_ptr<Task> m_owner;
+ shared_ptr<ExclusionState> m_state;
public:
+ Exclusion();
+
/// Attempt to obtain a Lock. If successful, current Task
/// owns the Lock until the ExclusionLock is released
/// (it is the ExclusionLock that owns the lock, so it can
/// sharing the existing owner. The Exclusion stays
/// locked until all ExclusionLocks are released.
///
- /// If not successful, the argument Task is appended to the
- /// Task that currently holds the lock (rendezvous). When
- /// the winning Task finishes execution, the Task
- /// infrastructure automatically reschedules the losing
- /// Task from the post-exec queue. The caller is expected
- /// to immediately release any other ExclusionLock objects
- /// it holds and exit its Task function so the restart
- /// begins from a clean state.
+ /// If not successful, the argument Task is parked on the
+ /// Exclusion's waiter list (rendezvous). When the last
+ /// ExclusionLock is released, every parked Task is
+ /// rescheduled via Task::run(). Waiters therefore wake
+ /// exactly when the lock actually becomes free — not when
+ /// the owning Task happens to finish an execution, which
+ /// re-woke them spuriously on every restart of the owner
+ /// and after every move_lock handoff. The caller is
+ /// expected to immediately release any other
+ /// ExclusionLock objects it holds and exit its Task
+ /// function so the restart begins from a clean state.
///
- /// Exclusion stores a weak_ptr<Task> as m_owner, NOT a
- /// reference to itself. The caller must keep the
- /// Exclusion alive (e.g. via shared_ptr) for the
- /// duration of the lock. If the Exclusion is managed
- /// by NamedPtr, the shared_ptr must be stored alongside
- /// the ExclusionLock — otherwise the NamedPtr entry
- /// expires and a new Exclusion is created for the same
- /// key, breaking mutual exclusion.
+ /// The ExclusionLock and parked waiters share the
+ /// Exclusion's internal state, so they remain valid even
+ /// if the Exclusion itself is destroyed while the lock is
+ /// held. If the Exclusion is managed by NamedPtr, the
+ /// shared_ptr should still be stored alongside the
+ /// ExclusionLock — otherwise the NamedPtr entry expires
+ /// and a new Exclusion is created for the same key,
+ /// breaking mutual exclusion.
ExclusionLock try_lock(const Task &task);
/// Return true if the Exclusion is currently owned by a
/// are outstanding, but only the new one gates further
/// try_lock calls.
///
- /// Tasks that contended on this Exclusion before move_lock was
- /// called were appended to the current Task's post-exec queue.
- /// They will be rescheduled when the current Task finishes,
- /// then retry try_lock and queue on @p new_task's TaskState.
+ /// Tasks that contended on this Exclusion before move_lock
+ /// was called stay parked on the Exclusion's waiter list.
+ /// They are rescheduled when the last ExclusionLock —
+ /// which after this call means @p new_task's — is
+ /// released; the handoff itself wakes nobody.
ExclusionLock move_lock(const Task &new_task);
};
m_barrier_state.reset();
}
- ExclusionLock::ExclusionLock(shared_ptr<Task> owner) :
+ /// Shared state behind one Exclusion: the current owner and the
+ /// Tasks parked waiting for it. Held by shared_ptr from the
+ /// Exclusion, every ExclusionOwner, and nothing else, so it
+ /// outlives the Exclusion object if locks are still outstanding.
+ class ExclusionState {
+ public:
+ mutex m_mutex;
+ weak_ptr<ExclusionOwner> m_owner;
+ list<Task> m_waiters;
+ };
+
+ /// The owner token shared by all ExclusionLocks of one
+ /// acquisition. Destroyed when the last ExclusionLock is
+ /// released; the destructor wakes the parked waiters — unless
+ /// ownership was retagged to a different live owner (move_lock),
+ /// in which case the lock is still held and nobody wakes.
+ class ExclusionOwner {
+ Task m_task;
+ shared_ptr<ExclusionState> m_state;
+ public:
+ ExclusionOwner(const Task &task, const shared_ptr<ExclusionState> &state) :
+ m_task(task),
+ m_state(state)
+ {
+ }
+ TaskId id() const { return m_task.id(); }
+ ~ExclusionOwner()
+ {
+ // Declared before the lock so it is destroyed after
+ // the mutex is released: if it is the last reference
+ // to a replacement owner, that owner's destructor
+ // must not run while we hold m_mutex.
+ shared_ptr<ExclusionOwner> replacement;
+ list<Task> waiters;
+ {
+ unique_lock<mutex> lock(m_state->m_mutex);
+ replacement = m_state->m_owner.lock();
+ if (replacement) {
+ // A different owner is registered and
+ // alive (move_lock retagged the
+ // Exclusion while our locks drained).
+ // The lock is still held; its release
+ // will wake the waiters.
+ return;
+ }
+ waiters.swap(m_state->m_waiters);
+ }
+ // Wake outside the state mutex: Task::run() takes
+ // task-infrastructure locks of its own.
+ for (const auto &t : waiters) {
+ t.run();
+ }
+ }
+ };
+
+ Exclusion::Exclusion() :
+ m_state(make_shared<ExclusionState>())
+ {
+ }
+
+ ExclusionLock::ExclusionLock(shared_ptr<ExclusionOwner> owner) :
m_owner(owner)
{
}
ExclusionLock
Exclusion::try_lock(const Task &task)
{
- unique_lock<mutex> lock(m_mutex);
- const auto sp = m_owner.lock();
+ // sp is declared before the lock so it is destroyed after
+ // the mutex is released: on the contention path it can be
+ // the last reference to the owner, and ~ExclusionOwner
+ // takes the same mutex.
+ shared_ptr<ExclusionOwner> sp;
+ unique_lock<mutex> lock(m_state->m_mutex);
+ sp = m_state->m_owner.lock();
if (sp) {
if (task && sp->id() == task.id()) {
// Reentrant: current task already owns this
return ExclusionLock(sp);
}
if (task) {
- sp->insert(task);
+ // Park the loser on the Exclusion. It is
+ // rescheduled when the last ExclusionLock is
+ // released. Dedupe by id: a task that dies
+ // repeatedly on the same lock parks once.
+ const auto task_id = task.id();
+ bool found = false;
+ for (const auto &t : m_state->m_waiters) {
+ if (t.id() == task_id) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ m_state->m_waiters.push_back(task);
+ }
}
return ExclusionLock();
} else {
- const auto rv = make_shared<Task>(task);
- m_owner = rv;
+ const auto rv = make_shared<ExclusionOwner>(task, m_state);
+ m_state->m_owner = rv;
return ExclusionLock(rv);
}
}
bool
Exclusion::locked_by_another(const Task &task)
{
- unique_lock<mutex> lock(m_mutex);
- const auto sp = m_owner.lock();
+ // Same destruction-order requirement as try_lock.
+ shared_ptr<ExclusionOwner> sp;
+ unique_lock<mutex> lock(m_state->m_mutex);
+ sp = m_state->m_owner.lock();
return sp && !(task && sp->id() == task.id());
}
ExclusionLock
Exclusion::move_lock(const Task &new_task)
{
- unique_lock<mutex> lock(m_mutex);
- const auto sp = m_owner.lock();
+ // Same destruction-order requirement as try_lock.
+ shared_ptr<ExclusionOwner> sp;
+ unique_lock<mutex> lock(m_state->m_mutex);
+ sp = m_state->m_owner.lock();
THROW_CHECK0(runtime_error, sp);
const auto current = Task::current_task();
THROW_CHECK2(runtime_error, sp->id(), current.id(),
sp->id() == current.id());
- // Retag the Exclusion: a copy of new_task sharing its TaskState
- // becomes the new owner. Contenders that arrive after this point
- // will queue on new_task's post-exec list. Callers must store the
- // returned ExclusionLock outside the new_task lambda (never capture
- // it in the closure) to avoid a TaskState → closure → lock → TaskState
- // cycle.
- const auto rv = make_shared<Task>(new_task);
- m_owner = rv;
+ // Retag the Exclusion: a new owner token bound to new_task
+ // takes over. The old owner's destructor sees the live
+ // replacement and wakes nobody; parked contenders stay on
+ // the waiter list until new_task's lock is released.
+ // Callers must store the returned ExclusionLock outside the
+ // new_task lambda (never capture it in the closure) to avoid
+ // a TaskState → closure → lock → TaskState cycle.
+ const auto rv = make_shared<ExclusionOwner>(new_task, m_state);
+ m_state->m_owner = rv;
return ExclusionLock(rv);
}