Is there any way I can lock a list of mutex?

221 Views Asked by At

My code is like the following. A Element holds a pointer to a mutex, and there is a vector that holds all Elements.

If I know the count of elements at compile time, I can construct a scoped_lock to lock all these mutexes. However, I don't know the size of the vector now. So how can I lock all the mutexes and generate a lock guard safely?

#include <vector>
#include <mutex>

struct Element {
    Element() {
        mut = new std::mutex();
    }
    Element(Element && other) : mut(other.mut) {
        other.mut = nullptr;
    }
    ~Element() { delete mut; }
    std::mutex * mut;
};

int main() {
    std::vector<Element> v;
    v.push_back(Element());
    v.push_back(Element());
    v.push_back(Element());

    std::scoped_lock(*v[0].mut, *v[1].mut, *v[2].mut);
}
3

There are 3 best solutions below

2
Fareanor On

I don't really see any use case where we would need to store a bunch of mutexes and lock/unlock them all in a row, but anyway, you could write your own "lock_guard" that will lock/unlock them all by iterating over the std::vector.

Note: If you cannot preserve the ordering of the locks, this solution will be subject to deadlock issues.


Example

Considering the following Element class (I just rewrote it in a simpler way):

struct Element
{
    std::unique_ptr<std::mutex> mut;

    Element() = default;
    Element(Element && other) : mut{std::move(other.mut)}
    {}
};

You could write your own "lock guard" as:

struct elements_lock
{
    std::vector<Element> & v_ref;

    elements_lock(std::vector<Element> & v) : v_ref{v}
    {
        for(Element & e : v)
            e.mut->lock();
    }
    ~elements_lock()
    {
        for(Element & e : v_ref)
            e.mut->unlock();
    }
};

And use it the following way:

int main()
{
    std::vector<Element> v(3); // Create a vector of 3 Elements
    {
        elements_lock el_lock(v);  // Acquire the locks sequentially

        // [...]

    } // At el_lock destruction, the locks will be released sequentially

    return 0;
}
0
Angelicos Phosphoros On

Well, you may try this cursed code:

struct MultiScopedLock {
  template <std::input_iterator I, std::sentinel_for<I> S>
  requires std::is_same_v<decltype(*std::declval<I>()), std::mutex &>
  MultiScopedLock(I begin, S end) {
    if constexpr (
        requires {requires std::random_access_iterator<I>; }
        && std::is_same_v<std::remove_cv<I>, std::remove_cv<S>>
    ){
        guards.reserve(end - begin);
    }
    try {
      for (; begin != end; ++begin) {
        guards.emplace_back(*begin);
      }
    } catch (...) {
      clear_guards();
      throw;
    }
  }

  // Need to delete all of this to guarantee unlocking order.
  MultiScopedLock(const MultiScopedLock &) = delete;
  MultiScopedLock(MultiScopedLock &&) = delete;
  MultiScopedLock &operator=(const MultiScopedLock &) = delete;
  MultiScopedLock &operator=(MultiScopedLock &&) = delete;

  ~MultiScopedLock() { clear_guards(); }

private:
  std::vector<std::unique_lock<std::mutex>> guards;

  void clear_guards() {
    // Release locks in reverse order.
    while (!guards.empty()) {
      guards.pop_back();
    }
  }
};

P.S. Thanks to @davis-herring for std::unique_lock suggestion.

0
fabian On

You could lock te mutexes in an order of your choosing and unlock them in reverse order. Note that this doesn't guarantee that the order of locking is suitable for avoiding a deadlock when std::scoped_lock is used with multiple of these mutexes.


class [[nodiscard]] ElementsLock
{
    std::vector<std::mutex*> m_orderedMutexes;
public:
    // defining these results in deleted copy semantics
    ElementsLock(ElementsLock&&) noexcept = default;
    ElementsLock& operator=(ElementsLock&&) noexcept = default;

    ElementsLock(std::vector<Element> const& v)
    {
        m_orderedMutexes.reserve(v.size());
        for (auto& e : v)
        {
            m_orderedMutexes.push_back(e.mut);
        }
        std::sort(m_orderedMutexes.begin(), m_orderedMutexes.end(),
            [](void const* a, void const* b) { return reinterpret_cast<uintptr_t>(a) < reinterpret_cast<uintptr_t>(b); });

        auto lockPos = m_orderedMutexes.begin();
        try
        {
            for (auto const end = m_orderedMutexes.end(); lockPos != end; ++lockPos)
            {
                (**lockPos).lock();
            }
        }
        catch (...)
        {
            // unlock locked mutexes
            for (auto const begin = m_orderedMutexes.begin(); lockPos != begin;)
            {
                --lockPos;
                try
                {
                    (**lockPos).unlock();
                }
                catch (...)
                {
                    // cannot properly unlock the locked mutexes
                    std::terminate();
                }
            }

            throw;
        }
    }

    ~ElementsLock() noexcept
    {
        for (auto begin = m_orderedMutexes.begin(), pos = m_orderedMutexes.end(); pos != begin;)
        {
            --pos;
            (**pos).unlock();
        }
    }
};


int main() {
    std::vector<Element> v;
    v.push_back(Element());
    v.push_back(Element());
    v.push_back(Element());

    ElementsLock lock(v);
}