2

Is there a portable implementation of the deadlock avoidance logic here (see the section marked `NON-PORTABLE'):

#include <cstdint>
#include <iostream>
#include <mutex>
#include <thread>

typedef long Money; //In minor unit.

class Account {
public:
    bool transfer(Account& to,const Money amount);
    Money get_balance() const;
    Account(const Money deposit=0) : balance{deposit} {}
private:
    mutable std::mutex lock;
    Money balance;
};

bool Account::transfer(Account& to,const Money amount){
    std::unique_lock<decltype(this->lock)> flock{this->lock,std::defer_lock};
    std::unique_lock<decltype(to.lock)> tlock{to.lock,std::defer_lock};
//NON-PORTABLE:BEGIN: using intptr_t AND assuming Total Strict Order.
    const auto fi{reinterpret_cast<const std::intptr_t>(static_cast<const void*>(&this->lock))};
    const auto ti{reinterpret_cast<const std::intptr_t>(static_cast<const void*>(&to.lock))};
    if(fi<ti){
        flock.lock();
        tlock.lock();
    } else if (fi!=ti) {
        tlock.lock();
        flock.lock();
    } else {
        flock.lock();
    }
//NON-PORTABLE:END  
    this->balance-=amount;
    to.balance+=amount;
    return true;
}

Money Account::get_balance() const{
    const std::lock_guard<decltype(this->lock)> guard{this->lock};
    return this->balance;
}

void hammer_transfer(Account& from,Account& to,const Money amount, const int tries){
    for(int i{1};i<=tries;++i){
        from.transfer(to,amount);
    }
}

int main() {
    constexpr Money open_a{ 200000L};
    constexpr Money open_b{ 100000L};
    constexpr Money tran_ab{10};
    constexpr Money tran_ba{3};
    constexpr Money tran_aa{7};

    Account A{open_a};
    Account B{open_b};
    
    std::cout << "A Open:" << A.get_balance() << '\n';
    std::cout << "B Open:" << B.get_balance() << '\n';
    
    constexpr long tries{20000}; 
    std::thread TAB{hammer_transfer,std::ref(A),std::ref(B),tran_ab,tries};
    std::thread TBA{hammer_transfer,std::ref(B),std::ref(A),tran_ba,tries};
    std::thread TAA{hammer_transfer,std::ref(A),std::ref(A),tran_aa,tries};

    TAB.join();
    TBA.join();
    TAA.join();

    const auto close_a{A.get_balance()};
    const auto close_b{B.get_balance()};   
    
    std::cout << "A Close:" << close_a<< '\n';
    std::cout << "B Close:" << close_b<< '\n';
    
    int errors{0};
    if((close_a+close_b)!=(open_a+open_b)){
        std::cout << "ERROR: Money Leaked!\n";
        ++errors;
    }
    if(close_a!=(open_a+tries*(tran_ba-tran_ab)) ||
          close_b!=(open_b+tries*(tran_ab-tran_ba))
    ){
        std::cout << "ERROR: 'Lost' Transaction(s)\n";
        ++errors;
    }
    if(errors==0){
        std::cout << "* SUCCESS *\n";
    }else{
        std::cout << "** FAILED **\n";
    }
    std::cout << std::endl;
    return 0;
}

Runnable here: https://ideone.com/hAUfhM

The assumptions are (and I believe sufficient – anyone?) that intptr_t exists and that the relational operators on intptr_t imply a Total Strict Ordering on the pointer values they represent.

That assumed ordering is not guaranteed and could be less portable than the non-portability of ordering of pointers (e.g. if intptr_t is wider than the pointer and not all bits are written).

I am aware of some different riffs on this and other designs. I'll upvote all good answers even if not portable that identify their assumptions about the implementation and ideally a platform where they apply and preferably one where they don't!

Persixty
  • 8,165
  • 2
  • 13
  • 35
  • 1
    Does this answer your question? [Locking multiple mutexes](https://stackoverflow.com/questions/13483767/locking-multiple-mutexes) – Passer By Nov 05 '20 at 11:25
  • Are you sure this is a language-lawyer question? Are you looking for quotes from the standard? – cigien Nov 05 '20 at 11:31
  • @cigien Yes. I'm inviting answers that identify their assumptions in my head that requires some language lawyering to identify what the standard doesn't promise such as while `intptr_t` has an order relation it's very clear that the standard makes no promise about semantics. – Persixty Nov 05 '20 at 11:36
  • 1
    Ok, that seems reasonable. Just checking :) – cigien Nov 05 '20 at 11:37
  • @PasserBy It's already been given as an answer and upvoted. But as mentioned it's no silver bullet. – Persixty Nov 05 '20 at 11:39
  • Then your question is extremely under-specified. You're asking people to guess at what in your mind constitutes an acceptable answer. – Passer By Nov 05 '20 at 11:46
  • @PasserBy I think I'm doing the opposite. `std::scoped_lock` is definitely a valid answer. I've upvoted 'std::lock' already. But many questions in Software Engineering have many answers which have different characteristics and suit different situations. Remember I may *not* even know what the restrictions/characteristics of your proposal are. So rather than have a long Q&A if you put them in the answer that's a good answer. Even if it doesn't suit me it may suit others. – Persixty Nov 05 '20 at 11:55
  • That's not how stackoverflow works. This is not a discussion forum, wiki, textbook or lecture. You have a well-specified question, and people answer with a solution that actually solves it. If you want software engineering techniques, you _may_ be looking for https://softwareengineering.stackexchange.com/. – Passer By Nov 05 '20 at 12:02
  • @PasserBy. It's how the owners want it to work 'we're working together to build a library of detailed answers to every question about programming'. https://stackoverflow.com/tour If I ask a narrow question people start telling you you should be doing something else. You do understand that I can't win! There's always someone who doesn't like the question and starts with saying its an 'X/Y problem'. You can't please everyone and today I haven't pleased you. My apologies. No harm intended. – Persixty Nov 05 '20 at 12:10
  • @PasserBy Whenever I ask questions there I get a similar response. Some say too vague, some say too specific. Even with all the facts some questions have multiple answers. I've already upvoted `std::lock` and of course `std::scoped_lock` doesn't fit because the question is tagged c++14 though of course it could be re-implemented or I could use the Boost version assuming I want to include Boost which I didn't mention either because I could consider it! Another valid answer. – Persixty Nov 05 '20 at 12:18
  • _"You do understand that I can't win"_, I think you misunderstood. When you have an answer that fits your question, but doesn't fit what you have _in your mind_, you are under-specifying. `std::lock` is C++11, but it doesn't fit what you want because _"it can be come counter productive and can show poor performance"_. – Passer By Nov 05 '20 at 12:36
  • @PasserBy Yes but many answers might fit my question. I just don't know yet. Of course the head line is 'ordering std::mutex' so `std::lock`, `std::scoped_lock` don't fit the question (they avoid ordering). But in a act of good faith and hoping to avoid the unproductive debates about the merits and wording of questions that I see Stackoverflow descend into all too often I'm prepared to accept them. Obviously my attempt to avoid the rabbit hole has entirely failed. – Persixty Nov 05 '20 at 12:40

4 Answers4

1

std::lock() has an buildin deadlock avoiding algorithm.

https://en.cppreference.com/w/cpp/thread/lock

gerum
  • 974
  • 1
  • 8
  • 21
  • It does and it's one of the 'other designs' I mention in the question. Duly upvoted. `std::lock()` is no silver bullet. At scale (with high levels of contention) it can be come counter productive and can show poor performance. But it's certainly a valid solution. – Persixty Nov 05 '20 at 11:34
  • 1
    @Persixty -- it's always true that if you know the peculiarities of your application's use of some resource you can write a resource manager that does a better job than a general-purpose resource manager that, for example, comes as part of the standard library. The design question is whether the extra work to write your own is worth the gain that you get from it, or whether the general-purpose code is good enough. – Pete Becker Nov 05 '20 at 13:02
  • I agree. There's a welcome answer about that. At low contention `std::lock` wins because `try_lock` overwhelmingly works first time! At relatively unequal contention locking the contended then try-lock works best and at grossly unequal high contention locking the uncontended then the bottleneck works best because you never get an unproductive lock on the bottleneck. But lock ordering also guarantees no un-productive locking and this question is a toy for a symmetric case. I turned it into an Account 'toy' because it doesn't need much background! – Persixty Nov 05 '20 at 13:18
1

Once you start to have lock contention you have lost with this method and need to rethink the whole solution. And nearly all locks causes a context switch that will cost around 20000 cycles each.

Usually most accounts have either many ingoing (shops, arrangements) or outgoing (pensions, dole etc.)

Once you have identified the contended account you can queue up a lot of transactions and then lock the contented account and run the transactions by try_lock the other account, if the lock succeed the transaction is done. Try the try_lock a couple of times then do the scope_lock with both locks for the remaining taking all transactions common for those two.

Part 2. How do I ensure an safe ordering of my locks as comparing pointers that are not into the same area is UB.

You add an unique ID to the account and compare on that instead!

Surt
  • 15,501
  • 3
  • 23
  • 39
  • I agree. Upvoted. When you look a real case use of an application you can often exploit it but asymmetric treatment. I've worked on a real system that used to lock the Direct Debit Account then do the days instructions. It worked until it went 24/7 and online blocked for 2 hours a night if you queried balance on the DD account. Another strategy is to lock the uncontended accounts then the contended to strictly minimise contending the 'hot' locking but give an opportunity to balance enquiries so everyone gets throughput. Still looking for a portable way to order `std::mutex`. – Persixty Nov 05 '20 at 12:58
  • 1
    @Persixty added a part 2 to answer your original question. – Surt Nov 05 '20 at 13:15
  • Also agree. The question exists precisely because of the UB. In a real accounting application we might use account-type to identify the Bank as contended and Customer as less so finally account-number (which would exist!) as tie-breaker. But strict ordering is great because it works (in principle) and practically minimises unproductive locking (that `std::lock` doesn't) and maximises throughput and utilisation without making assumptions regarding asymmetric lock demand. What's not to like? (apart from the UB!) – Persixty Nov 05 '20 at 13:32
1

tl;dr - you can make your original pointer comparison portably in C++20. I'd probably wrap that code into a scoped_ordered_lock or something though, because the code is still a bit hairy.


The assumptions are (and I believe sufficient – anyone?) that intptr_t exists and that the relational operators on intptr_t imply a Total Strict Ordering on values when holding values cast from valid non-null pointers to std::mutex.

Not precisely. You do always have a total strict order on the integral values. The problem arises when the mapping from intptr_t to pointer is many-to-one (this is the case for the segmented address example here - ie, TSO on intptr_t is not sufficient).

The pointer to intptr_t mapping must also be injective (it doesn't have to be a bijection, because we don't care if some intptr_t values are unused/don't represent valid pointers).

Anyway, it's obvious that a total strict ordering on pointers can exist: it's just implementation-specific. Segmented addresses can be normalized or flattened, etc.

Fortunately, a suitable implementation-defined total strict ordering is provided: by the 3-way functor std::compare_three_way in C++20, and by the 2-way functors less, greater etc. prior to C++20 (and maybe also in C++20).

There is no equivalent language about the implementation-defined strict total order over pointers in the text about the spaceship operator - even though compare_three_way is described as calling that - or about the other relational operators.

This seems to be deliberate, so that the builtin operators <, >, , <=, >=, and <=> don't acquire new constraints that might be expensive on some platform. Indeed, the 2-way relational operators are explicitly described as a partial order on pointers.

So, this should be identical to your original code, except portable:

const auto order = std::compare_three_way{}(&this->lock, &to.lock);
if(order == std::strong_ordering::less){
    flock.lock();
    tlock.lock();
} else if (order == std::strong_ordering::greater) {
    tlock.lock();
    flock.lock();
} else {
    flock.lock();
}

Note

  • as of C++20 (and specifically PDF:P1961R0), [comparisons.general] says

    For templates less, greater, less_­equal, and greater_­equal, the specializations for any pointer type yield a result consistent with the implementation-defined strict total order over pointers

    This is a weaker requirement which permits them to provide a partial order, so long as it never disagrees with the total order. It's not obvious whether this is a deliberate weakening, or it only intended to say that they must implement the same total order defined elsewhere.

  • prior to C++20 less etc. did require a total order for these functors.

In any case, if you don't have access to C++20 and compare_three_way, your less etc. are guaranteed to provide the total ordering you need. Just don't rely on the raw relational operators.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • 1
    So it is a defect in the standard that the pointer compare is not written in the std::compare_three_way documentation? Even though I would rather have that the language changed to the comparison would be allowed through normalization of pointers when comparing. – Surt Nov 05 '20 at 16:00
  • 1
    I think it is intentional that `compare_three_way` has special pointer semantics that are omitted from `operator <=>`. The implementation-defined total strict ordering is described as _"consistent with the **partial** order imposed by the builtin operators ... <=>"_. Perhaps it's to avoid making the operators more expensive on platforms with non-flat memory models. – Useless Nov 05 '20 at 16:20
  • Heroic answer. I'll edit to make clear about the ordering needing to project back onto `void*`. I think you may mean `std::compare_three_way{}(&this->lock, &to.lock)` because fi and ti are my intptr_t values which I still think may not work. Sadly my project is stuck on C++14 and struggling to enable even C++17 to test `std::has_unique_object_representations` so the issue could be detected. But here is your solution https://wandbox.org/permlink/KnhFA1n4FUWYA0tP. – Persixty Nov 05 '20 at 16:27
  • Oh you're quite right, thanks. I reduced your example slightly too much! – Useless Nov 05 '20 at 16:28
  • I'm sure you're right about expense. The pre-existing restrictions on pointer comparisons surely exist to not incur unnecessary de-normalized pointer handling (though `==` must) and the spaceship should follow that model (looks like The Joker to me). And that template exists when its needed (rarely). I'd still vote for a `std::normalize` template mind. – Persixty Nov 05 '20 at 16:44
  • _you can make your original pointer comparison portably in C++20_ `std::less` gives total ordering on pointers and exists since C++98. What do I miss? – Language Lawyer Nov 11 '20 at 06:50
  • `std::less` is defined to use `<` which is explicitly defined as providing a [_partial_ order](http://eel.is/c++draft/expr.rel#4) on pointers. It may in practice be total on platforms with flat memory models, but it isn't guaranteed by the standard. – Useless Nov 11 '20 at 13:44
  • If you want me to see your comment, you need to @mention me. And `std::less` is explicitly defined as providing [_strict total_ order](https://timsong-cpp.github.io/cppwp/n4659/comparisons#2) on pointers. – Language Lawyer Nov 11 '20 at 18:53
  • @LanguageLawyer - interesting, the eel.is version specifies that those functors are consistent with the TSO, but that still allows them to implement the partial ordering. I'll have to do some more research and update the answer – Useless Nov 12 '20 at 10:09
  • The difference between C++17 and the current wording is that "implementation-defined" was added and "strict total order over pointers" was moved from the paragraph into separate definition. _that still allows them to implement the partial ordering_ Uhm, what do you mean? If `std::less` will somehow violate strict total order rules, how it would be consistent with implementation-defined strict total order over pointers? – Language Lawyer Nov 12 '20 at 10:30
  • A partial order can be consistent with a total order, so long as it agrees with the total order on every pair where the partial order is defined. A total order is just an extension of a partial order which is actually defined on all pairs. – Useless Nov 12 '20 at 10:46
  • I don't see the paragraph saying «For templates ... the specializations for any pointer type, **for arguments for which ordinary comparison is not unspecified**, yield a result consistent with ...», which means that they shall yield result consistent with strict total order for every pair of arguments. Could you show an example how to yield a result consistent with strict total order for any pair of pointers and yet not provide strict total order for all pointers? – Language Lawyer Nov 12 '20 at 11:07
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/224464/discussion-between-useless-and-language-lawyer). – Useless Nov 12 '20 at 11:09
0

This is a self-answer to show the revised code. The credit is due to the accepted answer above. The learning for me is that since C++14 std::less, std::greater, etc. define a Strict Total Order on pointers that is consistent with the partial order already defined by < and > etc.

By using those templates, this code is now guaranteed to be deadlock-free. In C++20 it can be made neater and potentially faster with std::compare_three_way<>.

https://ideone.com/ekuf2f

#include <functional>    
#include <iostream>
#include <mutex>
#include <thread>

typedef long Money; //In minor unit.

class Account {
public:
    bool transfer(Account& to,const Money amount);
    Money get_balance() const;
    Account(const Money deposit=0) : balance{deposit} {}
private:
    mutable std::mutex lock;
    Money balance;
};

namespace{
    std::less<void*> less{};
    std::equal_to<void*> equal_to{};
}

bool Account::transfer(Account& to,const Money amount){
    std::unique_lock<decltype(this->lock)> flock{this->lock,std::defer_lock};
    std::unique_lock<decltype(to.lock)> tlock{to.lock,std::defer_lock};
    if(less(&this->lock,&to.lock)){
        flock.lock();
        tlock.lock();
    } else if(equal_to(&this->lock,&to.lock)) {
        flock.lock();
    } else {
        tlock.lock();
        flock.lock();
    }
    this->balance-=amount;
    to.balance+=amount;
    return true;
}

Money Account::get_balance() const{
    const std::lock_guard<decltype(this->lock)> guard{this->lock};
    return this->balance;
}

void hammer_transfer(Account& from,Account& to,const Money amount, const int tries){
    for(int i{1};i<=tries;++i){
        from.transfer(to,amount);
    }
}

int main() {
    constexpr Money open_a{ 200000L};
     constexpr Money open_b{ 100000L};
    constexpr Money tran_ab{10};
    constexpr Money tran_ba{3};
    constexpr Money tran_aa{7};

    Account A{open_a};
    Account B{open_b};
    
    std::cout << "A Open:" << A.get_balance() << '\n';
    std::cout << "B Open:" << B.get_balance() << '\n';
    
    constexpr long tries{20000}; 
    std::thread TAB{hammer_transfer,std::ref(A),std::ref(B),tran_ab,tries};
    std::thread TBA{hammer_transfer,std::ref(B),std::ref(A),tran_ba,tries};
    std::thread TAA{hammer_transfer,std::ref(A),std::ref(A),tran_aa,tries};

    TAB.join();
    TBA.join();
    TAA.join();

    const auto close_a{A.get_balance()};
    const auto close_b{B.get_balance()};   
    
    std::cout << "A Close:" << close_a<< '\n';
    std::cout << "B Close:" << close_b<< '\n';
    
    int errors{0};
    if((close_a+close_b)!=(open_a+open_b)){
        std::cout << "ERROR: Money Leaked!\n";
        ++errors;
    }
    if(close_a!=(open_a+tries*(tran_ba-tran_ab)) ||
          close_b!=(open_b+tries*(tran_ab-tran_ba))
    ){
        std::cout << "ERROR: 'Lost' Transaction(s)\n";
        ++errors;
    }
    if(errors==0){
        std::cout << "* SUCCESS *\n";
    }else{
        std::cout << "** FAILED **\n";
    }
    std::cout << std::endl;
    return 0;
}
Persixty
  • 8,165
  • 2
  • 13
  • 35