0

I have data structures (let's call each a "resource") that are stored in POSIX shared memory. Access to each resource is mediated by a per-resource mutex. A process may sometimes need to update several resources atomically. This process must acquire all of the prerequisite mutexes before updating/modifying the resources in question. The mutexes must be obtained in a well-defined order to avoid classic deadlock scenarios. I want to develop a compile-time method to ensure that locks are obtained in the correct order.

Each resource is mapped individually into each process in arbitrary order. For this reason, I cannot obtain the resources in resource-address order. Besides the fact that determining the proper order would not occur at compile-time, the relative order of resource addresses would likely differ from process to process, since each resource may (likely, even) be mapped to a different virtual address. Luckily, each resource type, represented by a struct, has a constexpr-defined unique integer ID. I want to obtain resources in ID-order.

Suppose each data structure looks something like this:

template<typename ResourceStruct, int UniqueId>
struct SharedResource
{
    static constexpr int ID = UniqueId;
    ResourceStruct resource;
};

I have a function similar to C++11's std::lock that receives the list of mutexes to lock as template parameters. I believe that it should be possible to sort these template parameters at compile-time according to each resource's ID. Unfortunately, I have been struggling with the necessary template meta-programming gymnastics to pull it off. I have studied several approaches (e.g., quicksort #1, quicksort #2) for sorting template parameters, but they all seem overly complicated. Am I over-thinking my problem? Is there an easier approach? If at all possible, I would prefer a pure C++11 solution (I would rather avoid dependencies on Boost).

Community
  • 1
  • 1
Glenn
  • 386
  • 3
  • 12
  • I would like to point out that should the process take any other lock (before or after) then all this is for naught... – Matthieu M. Oct 26 '15 at 18:38
  • Of course, but that is a danger with practically any cooperative synchronization scheme. For debugging purposes, I may keep a thread-local variable that flags when a thread holds a resource. Nested requests could be rejected (or at least logged). My main motivation is to promote a practice that should (hopefully) avoid bugs. A secondary motivation is efficiency: There is less opportunity for scheduler thrashing if groups of locks are released in reverse order. – Glenn Oct 26 '15 at 23:34
  • If you are ready to keep a global variable (thread-local or not), you might be interested in storing the latest ID still locked there. Then, whenever attempting to acquire a lock, you can check the unique ID of the resource and see whether it's authorized or not (note: in order to restore the previous ID locked, the lock itself should memorize it). – Matthieu M. Oct 27 '15 at 08:16
  • That's a great idea. Thanks! – Glenn Nov 10 '15 at 00:38
  • Compile-time sorting is a fun and challenging project. However your question presumes that locking in order is the best algorithm. If your mutexes have `try_lock` functionality it may not be. See http://howardhinnant.github.io/dining_philosophers.html for performance comparisons of the ordered locking algorithm vs several other multi-lock algorithms. – Howard Hinnant May 24 '16 at 01:16

3 Answers3

1

Take your ids. Pack them into a template sequence.

Turn each element into a pair of (value, index).

Write a compile-time sort on those elements. I find merge sort easy to write, or you could go with bubble or selection sort (presuming count is small). A quicksort is probably overkill.

Now, strip out the indexes. Generate a pack of these indexes, in the sorted order.

Wrap your original arguments up in a tuple, do std::get<Is> (where the Is are from the std::index_sequence containing the stripped out indexes) on them to get the newly ordered arguments, and call a function. The locks are now ordered.

Manually writing a sort is work. If you don't want to use a boost implementation, you have to write yourself a sort.


template<class...Ts> struct types {using type=types;};
template<class types, size_t N> struct get_nth; // ::type is the nth element of types
template<class types, size_t N> struct remove_nth; // ::type is types without the nth
template<class types, class pred> struct min_index; // returns the index of the least element
template<class...Types> struct append; // appens the types in Types... into one types<?>
template<class types, class pred> struct selection_sort;
// if non-empty, gets the min element, generates
// a types<> containing just it, and appends it to the front of
// the remaining elements with the nth element removed, then sorted
// if types is empty, returns `types<>`.

Should be about 100 lines of code at most. 200 if you like carriage returns.

Converting to pairs with the index bit just makes sure the values can follow the types around.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

Given that we are talking about compile-time sorts, I'll assume that the number of arguments is fairly low. In this case, then I would recommend forgetting about implementing a generic solution and simply use Sorting Networks.

It would be much easier in C++14 (due to automatic return type deduction), however it is still possible in C++11. It'll even be very nice to template depth limits:

template <typename T0, typename T1>
struct cmp { static uint64_t const value = T0::ID < T1::ID; };

//
//  Sort 1
//
template <typename T0>
struct sort_1 {
    typedef std::tuple<T0&> type;
    type sort(T0& t0) { return {t0}; }
};

//
//  Sort 2
//
template <uint8_t C, typename T0, typename T1>
struct sort_2_impl;

//  T0 >= T1
template <typename T0, typename T1>
struct sort_2_impl<0, T0, T1> {
    typedef std::tuple<T1&, T0&> type;
    type sort(T0& t0, T1& t1) { return {t1, t0}; }
};

//  T0 < T1
template <typename T0, typename T1>
struct sort_2_impl<1, T0, T1> {
    typedef std::tuple<T0&, T1&> type;
    type sort(T0& t0, T1& t1) { return {t0, t1}; }
};

template <typename T0, typename T1>
struct sort_2:
    sort_2_impl<
        cmp<T0,T1>::value,
        T0, T1
    > {};

//
//  Sort 3
//
template <uint8_t C, typename T0, typename T1, typename T2>
struct sort_3_impl;

//  0: T0 >= T1 & T0 >= T2 & T1 >= T2 -> T2 <= T1 <= T0
template <typename T0, typename T1, typename T2>
struct sort_3_impl<0, T0, T1, T2> {
    typedef std::tuple<T2&, T1&, T0&> type;
    type sort(T0& t0, T1& t1, T2& t2) { return {t2, t1, t0}; }
};

//  1: T0 < T1 & T0 >= T2 & T1 >= T2 -> T2 <= T0 < T1
template <typename T0, typename T1, typename T2>
struct sort_3_impl<1, T0, T1, T2> {
    typedef std::tuple<T2&, T0&, T1&> type;
    type sort(T0& t0, T1& t1, T2& t2) { return {t2, t0, t1}; }
};

//  2: T0 >= T1 & T0 < T2 & T1 >= T2 -> impossible

//  3: T0 < T1 & T0 < T2 & T1 >= T2 -> T0 < T2 <= T1
template <typename T0, typename T1, typename T2>
struct sort_3_impl<3, T0, T1, T2> {
    typedef std::tuple<T0&, T2&, T1&> type;
    type sort(T0& t0, T1& t1, T2& t2) { return {t0, t2, t1}; }
};

//  4: T0 >= T1 & T0 >= T2 & T1 < T2 -> T1 < T2 <= T0
template <typename T0, typename T1, typename T2>
struct sort_3_impl<4, T0, T1, T2> {
    typedef std::tuple<T1&, T2&, T0&> type;
    type sort(T0& t0, T1& t1, T2& t2) { return {t1, t2, t0}; }
};

//  5: T0 < T1 & T0 >= T2 & T1 < T2 -> impossible

//  6: T0 => T1 & T0 < T2 & T1 < T2 -> T1 <= T0 < T2
template <typename T0, typename T1, typename T2>
struct sort_3_impl<6, T0, T1, T2> {
    typedef std::tuple<T1&, T0&, T2&> type;
    type sort(T0& t0, T1& t1, T2& t2) { return {t1, t0, t2}; }
};

//  7: T0 < T1 & T0 < T2 & T1 < T2 -> T0 < T1 < T2
template <typename T0, typename T1, typename T2>
struct sort_3_impl<7, T0, T1, T2> {
    typedef std::tuple<T0&, T1&, T2&> type;
    type sort(T0& t0, T1& t1, T2& t2) { return {t0, t1, t2}; }
};

template <typename T0, typename T1, typename T2>
struct sort_3:
    sort_3_impl<
        (cmp<T0, T1>::value << 0) |
        (cmp<T0, T2>::value << 1) |
        (cmp<T1, T2>::value << 2),
        T0, T1, T2
    > {};

Oh, and it might be worth using a script to generate all that boilerplate...

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 1
    Using a code generator to implement the brute-force sorting network above, generating the networks to support tuples of up to 8 elements results in 370000+ lines of (readable) code. Since the vast majority of generated types are never used, parsing the generated code is a significant compile-time overhead. Tuples of 6 elements is reasonable at ~7000 lines of code. Tuples of 7 elements appears to be on the edge of reasonable at ~50000. – Glenn Nov 10 '15 at 04:11
0

Following the algorithm outlined by Yakk, and using an answer to this question, I have devised the following. I appreciate feedback! (Compiled with clang++ 7.0.0 with -std=c++11)

#include <iostream>
#include <type_traits>
#include <tuple>

namespace detail
{
    // pairs a tuple element with an index value
    template <typename TUP, std::size_t I>
    struct enumerated_tuple
    {
        static constexpr std::size_t index = I;
        using type = typename std::tuple_element<I, TUP>::type;
    };


    // implement index_sequence and make_index_sequence for C++11
    template <std::size_t...> struct index_sequence {};

    template <std::size_t N, std::size_t... Is>
    struct make_index_sequence : make_index_sequence<N - 1, N - 1, Is...> {};

    template <std::size_t... Is>
    struct make_index_sequence<0u, Is...> : index_sequence<Is...>
    {
         using type = index_sequence<Is...>;
    };


    // functions for binding an index to a tuple

    // returns a tuple of tuple elements paired with an index value
    template <typename TUP, std::size_t... I>
    auto make_indexed_tuple(TUP&& t, index_sequence<I...>) -> decltype(std::make_tuple(enumerated_tuple<TUP, I>()...))
    {
        return std::make_tuple(enumerated_tuple<TUP, I>()...);
    }

    // pairs each tuple element with an index
    template <typename TUP>
    struct indexed_tuple
    {
        using type = decltype(make_indexed_tuple(TUP(), typename make_index_sequence<std::tuple_size<TUP>::value>::type()));
    };


    // functions for generating index sequences used for removing a selected element from a tuple

    // join two sequences, with the second sequence shifted by Offset
    template <typename Seq1, std::size_t Offset, typename Seq2> struct concat_seq;
    template <std::size_t ... Is1, std::size_t Offset, std::size_t ... Is2>
    struct concat_seq<index_sequence<Is1...>, Offset, index_sequence<Is2...>>
    {
        using type = index_sequence<Is1..., (Offset + Is2)...>;
    };

    // generate a sequence 0..N without E, where E >= 0 and E <= N
    template <std::size_t N, std::size_t E>
    struct gen_seq
    {
        using type = typename detail::concat_seq<typename make_index_sequence<E>::type, E + 1, typename make_index_sequence<(N > E) ? (N - E - 1) : 0>::type>::type;
    };


    // generate a subtuple, picking out elements based upon the order and value of supplied integer sequence
    template <typename TUP, std::size_t... I>
    auto subtuple(TUP&& t, index_sequence<I...>) -> decltype(std::make_tuple(std::get<I>(t)...))
    {
        return std::make_tuple(std::get<I>(t)...);
    }

    // remove the nth element from a tuple
    template <typename TUP, std::size_t N>
    struct remove_nth
    {
        using type = decltype(subtuple(TUP(), typename gen_seq<std::tuple_size<TUP>::value, N>::type()));
    };

    // get the nth element from a tuple. (wrap std::tuple_element for the sake of consistency (flips template params))
    template <typename TUP, std::size_t N>
    struct get_nth
    {
        using type = typename std::tuple_element<N, TUP>::type;
    };

    // concatenates two tuples
    template <typename TUP1, typename TUP2>
    struct append
    {
        using type = decltype(std::tuple_cat(TUP1(), TUP2()));
    };

    // select the minimum type
    template <typename T0, typename T1, template<typename, typename> class CMP>
    struct select_min
    {
        using type = typename std::conditional<CMP<typename T0::type, typename T1::type>::value, T0, T1>::type;
    };

    // functions for finding the minimum element in a tuple
    template <typename TUP, std::size_t size, template<typename, typename> class CMP>
    struct min_tuple_element_helper
    {
        using type = typename select_min<typename get_nth<TUP, 0>::type, typename min_tuple_element_helper<typename remove_nth<TUP, 0>::type, size-1, CMP>::type, CMP>::type;
    };

    template <typename TUP, template<typename, typename> class CMP>
    struct min_tuple_element_helper<TUP, 1, CMP>
    {
        using type = typename std::tuple_element<0, TUP>::type;
    };

    // find the minimum tuple element, using the comparator CMP
    template <typename TUP, template<typename, typename> class CMP>
    struct min_tuple_element
    {
        using indexed = typename indexed_tuple<TUP>::type;
        using type_and_index = typename min_tuple_element_helper<indexed, std::tuple_size<indexed>::value, CMP>::type;
        using type = typename type_and_index::type;
        static constexpr std::size_t index = type_and_index::index;
    };

    template <typename TUP, std::size_t size, template<typename, typename> class CMP>
    struct selection_sort_helper
    {
        using index = typename indexed_tuple<TUP>::type;
        using selected = typename min_tuple_element<TUP, CMP>::type_and_index;
        using remaining = typename remove_nth<TUP, selected::index>::type;
        using remaining_sorted = typename selection_sort_helper<remaining, size-1, CMP>::type;
        using type = typename append<std::tuple<typename selected::type>, remaining_sorted>::type;
    };

    template <typename TUP, template<typename, typename> class CMP>
    struct selection_sort_helper<TUP, 1, CMP>
    {
        using type = TUP;
    };
} // end namespace



template <typename L, typename R>
struct less_than
{
    static constexpr bool value = (L::id < R::id);
};

template <typename L, typename R>
struct greater_than
{
    static constexpr bool value = (L::id > R::id);
};

// sort the elements in tuple, using the comparator CMP
template <typename TUP, template<typename, typename> class CMP>
struct selection_sort
{
    using type = typename detail::selection_sort_helper<TUP, std::tuple_size<TUP>::value, CMP>::type;
};

// all tuple elements are a concrete type of this type
template <std::size_t ID>
struct Value
{
    static constexpr std::size_t id = ID;
};

int main(void)
{
    using example = typename std::tuple<Value<5>, Value<3>, Value<2>, Value<4>>;
    printf("unsorted tuple:\n");
    printf("%d\n", (int)std::tuple_element<0, example>::type::id);
    printf("%d\n", (int)std::tuple_element<1, example>::type::id);
    printf("%d\n", (int)std::tuple_element<2, example>::type::id);
    printf("%d\n", (int)std::tuple_element<3, example>::type::id);

    using min_sorted_example = typename selection_sort<example, less_than>::type;
    printf("min-sorted tuple:\n");
    printf("%d\n", (int)std::tuple_element<0, min_sorted_example>::type::id);
    printf("%d\n", (int)std::tuple_element<1, min_sorted_example>::type::id);
    printf("%d\n", (int)std::tuple_element<2, min_sorted_example>::type::id);
    printf("%d\n", (int)std::tuple_element<3, min_sorted_example>::type::id);

    using max_sorted_example = typename selection_sort<example, greater_than>::type;
    printf("max-sorted tuple:\n");
    printf("%d\n", (int)std::tuple_element<0, max_sorted_example>::type::id);
    printf("%d\n", (int)std::tuple_element<1, max_sorted_example>::type::id);
    printf("%d\n", (int)std::tuple_element<2, max_sorted_example>::type::id);
    printf("%d\n", (int)std::tuple_element<3, max_sorted_example>::type::id);

    return 0;
}

Output:

unsorted tuple:
5
3
2
4
min-sorted tuple:
2
3
4
5
max-sorted tuple:
5
4
3
2
Community
  • 1
  • 1
Glenn
  • 386
  • 3
  • 12