【问题标题】:Deadlock avoidance by ordering std::mutex通过订购 std::mutex 避免死锁
【发布时间】:2020-11-05 11:18:35
【问题描述】:

这里是否有可移植的避免死锁逻辑的实现(参见标记为“不可移植”的部分):

#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;
}

可在此处运行:https://ideone.com/hAUfhM

这些假设是(我相信足够了——有人吗?)intptr_t 存在并且intptr_t 上的关系运算符暗示了对它们所代表的指针值的完全严格排序。

假设的排序并不能保证,并且可能不如指针排序的不可移植性那么可移植(例如,如果 intptr_t 比指针宽,并且并非所有位都被写入)。

我知道这个设计和其他设计有一些不同的即兴演奏。 我会投票赞成所有好的答案,即使不是可移植的,也可以确定他们对实施的假设,理想情况下是他们适用的平台,最好是他们不适用的平台!

【问题讨论】:

  • 这能回答你的问题吗? Locking multiple mutexes
  • 您确定这是一个语言律师问题吗?您在寻找标准的报价吗?
  • @cigien 是的。我正在邀请答案来确定他们在我脑海中的假设,这需要一些语言律师来确定标准不承诺的内容,例如虽然intptr_t 具有顺序关系,但很明显该标准对语义没有承诺。跨度>
  • 好吧,这似乎是合理的。只是检查:)
  • @PasserBy 它已经作为答案给出并被投票赞成。但如前所述,这不是灵丹妙药。

标签: c++ concurrency c++14 language-lawyer portability


【解决方案1】:

tl;dr - 您可以在 C++20 中进行原始指针比较。我可能会将该代码包装成 scoped_ordered_lock 或其他东西,因为代码仍然有点毛茸茸。


假设是(我相信足够了——任何人?)intptr_t 存在,并且 intptr_t 上的关系运算符在持有从有效非空指针转换为 std::mutex 的值时对值进行完全严格排序。

不准确。你确实总是对整数值有一个完全严格的顺序。当从intptr_t 到指针的映射是多对一时,就会出现问题(分段地址示例here 就是这种情况——即intptr_t 上的TSO 是不够的)。

指向intptr_t 映射的指针也必须是单射的(它不必是双射,因为我们不关心某些intptr_t 值是否未使用/不代表有效指针)。

无论如何,很明显,指针的完全严格排序可以存在:它只是特定于实现的。分段地址可以归一化或展平等。

幸运的是,提供了一个合适的实现定义的完全严格排序:由 C++20 中的 3 路函子 std::compare_three_way 和 C 之前的 2 路函子 lessgreater 等提供++20(也许也在 C++20 中)。

在关于 spaceship operator 的文本中没有关于 implementation-defined strict total order over pointers 的等效语言 - 尽管 compare_three_way 被描述为调用它 - 或关于其他关系运算符。

这似乎是故意的,因此内置运算符 &lt;&gt;、、&lt;=&gt;=&lt;=&gt; 不会获得在某些平台上可能很昂贵的新约束。实际上,2 路关系运算符明确描述为指针上的partial order

因此,这应该与您的原始代码相同,除了可移植性:

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();
}

注意

  • 从 C++20 开始(特别是 PDF:P1961R0),[comparisons.general] 说

    对于模板lessgreaterless_­equalgreater_­equal,任何指针类型的特化都会产生与实现定义的指针上的严格总顺序一致的结果

    这是一个较弱的要求,允许他们提供部分订单,只要它永远不会与总订单不一致。不清楚这是故意削弱,还是只是说他们必须执行其他地方定义的相同总顺序。

  • 在 C++20 之前less 等。确实需要这些函子的总顺序。

在任何情况下,如果您无法访问 C++20 和 compare_three_way,您的 less 等。保证提供您需要的全部订购。只是不要依赖原始的关系运算符。

【讨论】:

  • 那么在 std::compare_three_way 文档中没有写指针比较是标准的缺陷吗?即使我宁愿通过比较时指针的规范化来允许将语言更改为比较。
  • 我认为compare_three_way 具有在operator &lt;=&gt; 中省略的特殊指针语义是有意的。实现定义的完全严格排序被描述为“与内置运算符施加的部分顺序一致... ”。也许是为了避免在非平面内存模型的平台上使操作符变得更加昂贵。
  • 英勇的回答。我将进行编辑以明确需要投影回void* 的顺序。我认为您的意思可能是std::compare_three_way{}(&amp;this-&gt;lock, &amp;to.lock),因为 fi 和 ti 是我的 intptr_t 值,我仍然认为它们可能不起作用。可悲的是,我的项目卡在 C++14 上,甚至连 C++17 都无法测试std::has_unique_object_representations,因此可以检测到问题。但这是您的解决方案wandbox.org/permlink/KnhFA1n4FUWYA0tP
  • 哦,你说的很对,谢谢。我稍微减少了你的例子!
  • 我相信您对费用的看法是正确的。对指针比较的预先存在的限制肯定存在,不会导致不必要的非规范化指针处理(尽管== 必须),并且宇宙飞船应该遵循该模型(在我看来就像小丑)。并且该模板在需要时(很少)存在。我仍然会投票给std::normalize 模板。
【解决方案2】:

std::lock() 有一个内置的死锁避免算法。

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

【讨论】:

  • 确实如此,它是我在问题中提到的“其他设计”之一。妥妥的投了赞成票。 std::lock() 不是灵丹妙药。在规模上(具有高水平的争用),它可能会适得其反并且表现出较差的性能。但这肯定是一个有效的解决方案。
  • @Persixty -- 如果你知道你的应用程序使用某些资源的特殊性,你可以编写一个比通用资源管理器做得更好的资源管理器,例如, 作为标准库的一部分。设计问题是自己编写的额外工作是否值得您从中获得收益,或者通用代码是否足够好。
  • 我同意。有一个受欢迎的答案。在低竞争时,std::lock 获胜,因为try_lock 绝大多数都是第一次工作!在相对不等的争用锁定情况下,争用然后 try-lock 工作得最好,而在严重不等的高争用情况下,锁定非争用然后瓶颈工作最好,因为你永远不会在瓶颈上获得非生产性锁定。但是锁排序也保证没有非生产性的锁定,这个问题对于对称情况来说是一个玩具。我把它变成了一个帐户“玩具”,因为它不需要太多背景!
【解决方案3】:

一旦你开始出现锁争用,你就失去了这种方法,需要重新考虑整个解决方案。而且几乎所有的锁都会导致上下文切换,每个会花费大约 20000 个周期。

通常大多数账户要么有很多流入(商店、安排),要么有很多流出(养老金、救济金等)

一旦您确定了竞争帐户,您可以将大量事务排队,然后锁定满足的帐户并通过 try_lock 另一个帐户运行事务,如果锁定成功,则事务完成。尝试 try_lock 几次,然后使用两个锁执行 scope_lock,其余的会获取这两个锁的所有事务。

第 2 部分。 我如何确保我的锁的安全排序,因为比较不在同一区域的指针是 UB。

您向帐户添加一个唯一 ID,然后进行比较!

【讨论】:

  • 我同意。赞成。当您查看应用程序的真实案例使用时,您通常可以利用它但不对称处理。我曾在一个真实的系统上工作过,该系统用于锁定直接借记账户,然后执行日期说明。如果您在 DD 帐户上查询余额,它一直工作到 24/7 并且每晚在线阻塞 2 小时。另一种策略是锁定非竞争帐户,然后是竞争帐户,以严格减少竞争“热”锁定,但提供平衡查询的机会,以便每个人都能获得吞吐量。仍在寻找一种便携的方式来订购std::mutex
  • @Persixty 添加了第 2 部分来回答您的原始问题。
  • 也同意。这个问题的存在正是因为 UB。在真实的会计应用程序中,我们可能会使用 account-type 来将 Bank 标识为有争议的,并将 Customer 标识为 less,因此最终将 account-number(它会存在!)作为 tie-breaker。但是严格排序很好,因为它(原则上)有效并且实际上最小化了非生产性锁定(std::lock 没有)并最大化吞吐量和利用率,而无需对不对称锁定需求进行假设。有什么不喜欢的? (除了 UB!)
【解决方案4】:

这是显示修改后代码的自我回答。功劳归功于上面接受的答案。 我的学习是因为 C++14 std::lessstd::greater 等在指针上定义了一个 Strict Total,它与 &lt;&gt; 等已经定义的部分顺序一致。

通过使用这些模板,现在可以保证这段代码没有死锁。 在 C++20 中,使用 std::compare_three_way&lt;&gt; 可以使其更简洁,甚至更快。

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;
}

【讨论】:

    猜你喜欢
    • 2017-12-17
    • 1970-01-01
    • 1970-01-01
    • 2020-10-25
    • 2012-10-30
    • 1970-01-01
    • 2017-12-02
    • 1970-01-01
    • 2013-06-07
    相关资源
    最近更新 更多