【问题标题】:c++ Why shouldn't I unlock a mutex from a different threadc++ 为什么我不应该从不同的线程解锁互斥锁
【发布时间】:2020-05-28 15:46:56
【问题描述】:

为什么我不应该从不同的线程解锁互斥锁?在 c++ 标准中,它说得很清楚:如果互斥锁当前没有被调用线程锁定,它会导致未定义的行为。但据我所知,在 Linux(Fedora 31 with GCC)上一切正常。我认真地尝试了一切,但我无法让它表现得异常。 我所要求的只是一个示例,其中从不同线程解锁互斥锁会影响某些东西,实际上是任何东西。

这是我写的一个快速测试,它是超级错误的,不应该工作,但它确实有效:

std::mutex* testEvent;

int main()
{
    testEvent = new std::mutex[1000];

    for(uint32_t i = 0; i < 1000; ++i) testEvent[i].lock();

    std::thread threads[2000];

    auto lock = [](uint32_t index) ->void { testEvent[index].lock(); assert(!testEvent[index].try_lock()); };
    auto unlock = [](uint32_t index) ->void { testEvent[index].unlock(); };

    for(uint32_t j = 0; j < 1000; ++j)
    {
        for(uint32_t i = 0; i < 1000; ++i)
        {
            threads[i]      = std::thread(lock,i);
            threads[i+1000] = std::thread(unlock,i);
        }
        for(uint32_t i = 0; i < 2000; ++i)
        {
            threads[i].join();
        }
        std::cout << j << std::endl;
    }

    delete[] testEvent;
}

【问题讨论】:

  • 但据我所知,在 Linux 上一切正常(Fedora 31 with GCC) 未定义程序的结果集包含您正在寻找的结果.这是 UB 中最危险的部分,它看起来可以完美运行。
  • 如果一切都按预期工作有什么问题吗? 是的。你基本上有一个活弹。它可能多年没有做任何事情,或者它可能会在你第一次发生变化时炸毁你的脸,比如在不同的机器上运行或使用不同的编译器进行编译。
  • UB 在启用优化器时往往会咬得最紧。并且不同的编译器(或相同编译器的不同版本)或不同平台上的相同编译器可能会生成不同的代码并表现不同。即使添加或删除不相关的代码也可能导致编译器改变行为,因为未定义的是整个程序,而不仅仅是包含 UB 的部分。简而言之;你只是不再对你的程序行为有任何保证,这不是你想要的情况。它是一个可以在任何时间爆炸的定时炸弹任何变化。
  • 每次我发现有人这样做都是因为他们不知道他们可以通过监视器(条件变量)得到他们想要的东西。

标签: c++ linux mutex undefined-behavior


【解决方案1】:

正如你已经说过的,它是 UB。 UB 表示它可以工作。或不。或者在工作和让你的电脑自己唱摇篮曲之间随机切换。 (另见“鼻恶魔”。)

以下是一些在 Fedora 31 上使用 GCC 在 x86-64 上破坏您的程序的方法:

  1. 使用-fsanitize=thread 编译。它现在每次都会崩溃,这仍然是一个有效的 C++ 实现,因为 UB。
  2. 在 helgrind (valgrind --tool=helgrind ./a.out) 下运行。它每次都会崩溃——仍然是托管 C++ 程序的有效方式,因为 UB。
  3. 目标系统上的 libstdc++/glibc/pthread 实现从默认使用“快速”互斥锁切换为“错误检查”或“递归”互斥锁 (https://manpages.debian.org/jessie/glibc-doc/pthread_mutex_init.3.en.html)。请注意,这可能以与您的程序 ABI 兼容的方式实现,这意味着它甚至不需要重新编译就可以突然停止工作。

话虽这么说,由于您使用的平台是 C++ 互斥体归结为 futex 实现的“快速”pthread 互斥体,因此这不会意外地起作用。只是不能保证在任何时候或在任何实际检查您是否在做正确的事情的情况下都可以继续工作。

【讨论】:

    【解决方案2】:

    我真的很想知道你为什么要首先这样做;)

    通常你会想要类似的东西

    lock();
    do_critical_task();
    unlock();
    

    (在 c++ 中,锁定/解锁通常通过使用 std::lock_guard 或类似名称来隐藏。)
    让我们假设一个线程(比如说线程 A)调用了这段代码并且在关键任务中,即它也持有锁。 那么如果你从另一个线程解锁同一个互斥体,除了A之外的任何线程也可以同时进入临界区。

    互斥体的主要目的是互斥(因此得名),所以你要做的就是抹去互斥体的目的;)

    也就是说:您应该始终相信标准。仅当某些系统在某个系统上运行时,它并不意味着它是可移植的。另外:特别是在并发上下文中,很多事情可以解决一千次,然后在竞争条件下第 1001 次失败。 在数学中,您的尝试可以与“通过示例证明”相媲美。

    【讨论】:

    • 互斥锁是一种适当的资源类型,有时您希望在线程之间切换资源。可能需要锁定保护某些数据的互斥体,准备该数据,然后将锁定传递给将继续处理数据的另一个线程。该标准不允许这样做,它需要一组额外的解锁/锁定来“转移”锁的所有权。互斥锁不仅仅保护关键部分。
    • 好吧,在我正在处理的应用程序中,它当前充当一个事件。互斥锁在构造时被锁定,然后当您尝试锁定它时,它会等到有人从另一个线程解锁它。这是非常错误的,但它已经工作了 6 年,所以不知道该怎么想。
    • @A.Hristov 看来你认为它“非常错误”,这是正确的印象。
    • @FrançoisAndrieux:是的,你是对的,没想到这一点。我什至倾向于说 C++ 委员会也只考虑了关键部分(或者至少他们不想宣传任何其他用途),因为您甚至无法通过 std::move 传输整个互斥锁;)跨度>
    • @A.Hristov:嗯,使用条件变量(带有实际条件来规避虚假唤醒)或信号量等待不是更好吗?
    猜你喜欢
    • 2011-07-24
    • 2012-12-25
    • 1970-01-01
    • 2010-11-18
    • 2020-12-07
    • 1970-01-01
    • 2022-07-31
    • 2010-11-22
    相关资源
    最近更新 更多