【问题标题】:What happens when two threads attempt a try_lock() on the SAME mutex at the EXACT same time?当两个线程同时在同一个互斥锁上尝试 try_lock() 时会发生什么?
【发布时间】:2019-11-15 11:54:21
【问题描述】:

这是我的第二篇文章。我的第一篇文章是作为 C 类入门课程的本科生。我希望这次我能做得更好。

我正在为我的研究开发一个随机模型,并且我正在寻找提高算法计算效率的方法。我最近被一位同事介绍了多线程,并且我已经成功地将它实现到我的一些不需要锁的算法中......

现在我正在尝试修改/线程化需要锁的算法。在我的文学/StackOverflow/google 搜索中,我学到了很多关于互斥锁类的知识。但是,我面临的问题似乎是独一无二的。就上下文而言,我能找到的最接近答案的是:

What happens when two threads ATTEMPT to lock the same resource at the exact same time?

它仍然没有直接解决我要问的问题。

我一直在尝试使用动态分配的指向互斥对象的指针数组来控制线程之间的内存访问。我的代码编译时没有警告/错误(编译 w/flags -pthread -std=c++11),并且在抛出 Segfault 的线程算法之前完美执行。在使用 lldb 进行调查后,我发现抛出的异常如下:

lldb output screenshot

经过进一步调查,我发现 lldb 输出所引用的线程都在尝试对完全相同的互斥对象执行 try_lock(),因为包含该对象地址的指针数组已传递给每个线程。

我的问题,与前面提到的帖子类似:

如果多个线程(多个线程)在完全相同的时间(处理器时钟的同一冲程)在同一个互斥体上尝试 try_lock(),会发生什么情况? mutex 类的较新实现是否有针对这种看似灾难性事件的解决方法(即 shard_mutex、timed_mutex 等)?

这最近让我大吃一惊,所以任何见解都将不胜感激。非常感谢 S/O 社区的所有帮助,在这篇文章和所有其他对我作为程序员的成长非常宝贵的内容上。

代码链接:

https://github.com/tylerbalbright/StackOverflow_7_4.git

RVEdata.cpp 中的第 751 行或第 857 行发生错误。

已修复但未解决:

我能够使用互斥对象的双端队列来修复我的代码,而不是创建指向动态创建的互斥对象的指针向量。该解决方案已由其他用户在这里提出:

How can I use something like std::vector<std::mutex>?

在我的第一次(不成功)试验中,我试图创建一个指向互斥体的指针数组,如下所示:

long int N = RuntimeVector.size(); //Varying size at runtime
std::mutex *MutexPtrs;
MutexPtrs = new std::mutex[N];

然后我会将新创建的数组作为指针传递给函数,该指针会将指向数组的指针传递给新线程,如下所示:

void SomeFunction(std::mutex *PosLocks[])
{
.
..
...

    SearchersPos.push_back(std::async(std::launch::async, &RVEdata::PositiveSearch, this, PosLocks));
}

使用此方法,代码不会在每次执行时都失败,但在使用 EXC_BAD_ACCESS 时,90% 的时间都会失败。有趣的是,在每次失败的执行中,当多个线程试图 try_lock 同一个互斥体时,都会引发错误的访问。它从来没有失败过,只有 1 个线程在一个单独的互斥锁上尝试 try_lock。当我在我们的 HPC 上运行相同的代码时,大约 95% 的时间都发生了故障,但是我在 gdb 中找不到像在 lldb 中那样多的信息(我对命令行 gdb 不太熟练)。

PS - 我在 macOS High Sierra 10.13.6、Apple LLVM 版本 10.0.0 (clang-1000.11.45.5) 上运行。

【问题讨论】:

  • 我很确定锁定(我在这里说的是模糊的记忆)是通过操作系统作为原子操作完成的,这意味着不会有重叠。不过,如果有人能纠正我,我会很高兴。
  • 这应该可以正常工作。这就是互斥锁的意义所在。我们需要显示错误的代码来了解错误在哪里
  • 重点是只有 ONE 线程会成功。为这个确切的用例明确定义了互斥锁。该实现将使用平台的适当(通常是硬件)机制来保证只有一个线程获取锁。
  • 注意:BAD_ACCESS 通常是指操作系统未分配给您的应用程序的内存地址。这意味着您可能在某处有一个错误的指针。
  • 一个线程会成功,另一个线程不会成功。您的段错误与此无关,是因为您的代码中存在其他错误。这不是任何类型的“灾难性事件”,根据定义,这正是互斥锁旨在处理的事情。这是他们的工作,做这种事情。

标签: c++ multithreading mutex undefined-behavior


【解决方案1】:

当多个线程同时尝试锁时,其中一个最终获得锁,而其他线程失败。

你的公主 bug是在另一个城堡由别的东西引起的。

【讨论】:

    【解决方案2】:

    如果多个线程(多个线程)在完全相同的时间(处理器时钟的同一冲程)在同一个互斥锁上尝试 try_lock(),会发生什么? mutex 类的较新实现是否有针对这种看似灾难性事件的解决方法(即 shard_mutex、timed_mutex 等)?

    绝对没有什么灾难性的,这就是互斥锁的用途。您没有发现互斥锁的工作方式存在缺陷,select 没有损坏。您应该阅读 StackOverflow 的一位创始人的The First Rule of Programming

    在我的第一次(不成功)试验中,我试图创建一个指向互斥体的指针数组,如下所示:

    问题是您正在创建两个互斥体数组

    std::mutex *PosLocks;
    std::mutex *GndLocks;
    
    PosLocks = new std::mutex[N];
    GndLocks = new std::mutex[N];
    

    但是你传递了数组的地址

    NetExists = FindNetwork(N, &PosLocks, &GndLocks);
    

    调用这个函数:

    bool RVEdata::FindNetwork(long int N, std::mutex *PosLocks[], std::mutex *GndLocks[])
    

    现在您已将std::mutex** 参数传递给函数,它可以是指向互斥体数组的指针 指向互斥体的指针数组。

    然后您以 mutex* 数组的形式访问它们,而不是 指向 mutex[] 的指针:

        if (PosLocks[i]->try_lock() == true)
    

    PosLocks[i] 对数组进行索引以获得mutex*,然后使用-&gt; 运算符取消引用指针。但那是倒退!你没有mutex*的数组!

    正如我在上面的评论中所说,您只是错误地访问了数组。这意味着程序会尝试将mutex 对象锁定在没有mutex 对象的某个地址,因为您正在读取一个位于互斥对象中间的地址,而不是在它的开头。

    上面的行应该首先取消引用指针,以获取mutex[],然后然后索引到数组中:

        if ((*PosLocks)[i].try_lock() == true)
    

    或者更好的是首先停止传递指向数组的指针,即将函数声明为:

    bool RVEdata::FindNetwork(long int N, std::mutex PosLocks[], std::mutex GndLocks[])
    

    并将其称为:

    NetExists = FindNetwork(N, PosLocks, GndLocks);
    

    现在你可以直接访问数组了:

        if (PosLocks[i].try_lock() == true)
    

    更好的是,停止使用动态创建的数组:

    std::vector<std::mutex> PosLocks;
    std::vector<std::mutex> GndLocks;
    // ...
    NetExists = FindNetwork(N, PosLocks, GndLocks);
    // ...
    
    bool RVEdata::FindNetwork(long int N, std::vector<std::mutex>& PosLocks, std::vector<std::mutex>& GndLocks)
    {
        // ...
        if (PosLocks[i].try_lock() == true)
    

    如果您没有混合使用数组和指针以及动态分配,问题就不会发生。

    【讨论】:

    • 感谢您的深入分析。我的代码为什么会这样表现是完全有道理的。至于答案的最后一部分,使用向量存储互斥对象是我的第一次尝试(不会编译),因为默认情况下互斥对象不可复制。 STL 容器既是我的神剑,也是我存在的祸根。在被介绍给他们之后,我似乎经常滥用基本的动态分配。
    • 所以不要复制它们。通过引用传递vector&lt;mutex&gt;,然后它不会被复制。问题不在于创建vector&lt;mutex&gt;,而是您之后尝试使用它进行的操作。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-23
    • 1970-01-01
    • 2014-03-18
    • 2014-08-31
    相关资源
    最近更新 更多