parallel101 / course

高性能并行编程与优化 - 课件
https://space.bilibili.com/263032155
Other
3.62k stars 532 forks source link

try_lock() 异常 #22

Closed YZH-bot closed 1 year ago

YZH-bot commented 1 year ago

我在ubuntu20.04上尝试跑了一下07这个代码,出来结果有点奇怪,请问这是什么原因?

#include <cstdio>
#include <mutex>
std::mutex mtx1;
int main() {
    if (mtx1.try_lock())
        printf("succeed\n");
    else
        printf("failed\n");
    if (mtx1.try_lock())
        printf("succeed\n");
    else
        printf("failed\n");
    mtx1.unlock();
    return 0;
}

输出结果都是succeed,按理应该先是succeed后failed

succeed
succeed
archibate commented 1 year ago

一、同一个线程重复 lock 是未定义行为

lock 的文档中我们可以看到:

If lock is called by a thread that already owns the mutex, the behavior is undefined: for example, the program may deadlock.

翻译:如果一个线程已经拥有锁,那么再调用一次 lock 是未定义行为:例如,可能发生死锁。

这是很容易理解的,因为在多线程编程模型中我们知道,同一个线程连续 lock 两次会导致死锁。但是标准说了这是未定义行为,而不是明确规定会发生死锁,所以同一个调用两次 lock 可能发生死锁,也可能发生其他事情:包括但不限于抛出异常后推出,段错误,除零错误,代码的执行顺序紊乱,改变其他变量的值,甚至电脑着火,都有可能发生,当然也可能不产生任何错误,正常执行下去。

这就是未定义行为,只要你触犯了,他可以发生任何事,C++ 标准都不保护你。标准规定为未定义的,编译器都可以随便处理,发生任何事情都是你自己的责任,只不过对于 double-lock 而言大概率是“死锁”这一种情况罢了,正如空指针解引用大概率是”段错误“这一种情况一样。

之所以标准不规定是为了不要限制编译器厂商的创造力,例如官方文档后面举了个例子:“一个编译器实现可以检测到这种问题,并抛出 system_error 异常,方便用户调试。”

An implementation that can detect the invalid usage is encouraged to throw a std::system_error with error condition resource_deadlock_would_occur instead of deadlocking.

二、同一个线程重复 try_lock 也是未定义行为

try_lock 的文档中我们还可以看到:

If try_lock is called by a thread that already owns the mutex, the behavior is undefined.

翻译:如果一个线程已经拥有锁,那么再调用一次 try_lock 也是未定义行为。

没想到8,try_lock 也不允许。

你是不是已经想当然地认为 lock 会死锁,try_lock 就不会,即使是同一个线程,也能安全返回 false?

你是不是已经想当然地认为 try_lock 的内部实现是这样的了:

bool try_lock() {
  if (!this->m_locked) {
    lock();
    return true;
  }
  return false;
}

如果标准都规定死了内部实现,不给编译器自由度,人家还怎么优化了?首先你“想当然”的这种 try_lock 内部实现就是不线程安全的。

记得我说的吗?发生未定义行为时编译器可以做任何事,包括不产生任何错误正常执行,但是打印一个错误的答案给你。

永远不要想当然,多看看 cppreference,官方文档说是未定义行为,就碰都不要去碰。不要依赖未定义行为,不要跟我说“汇编就是这样”,“我平时都是能稳定触发死锁/段错误/改变其他变量值”的。调试代码如果遇到发生任何离谱的行为时,首先检查你的代码,是不是有什么地方“想当然”了,触发未定义行为了,那发生任何事情都是合理的。

这就是 C++,听小彭老师说

所以如何修复这个未定义行为呢?标准说同一个线程多次调用 lock 或 try_lock 是未定义行为,但是他没说多个线程分别调用啊?所以你可以创建另一个线程去调用 try_lock,达到你“想当然”的那种“教学结果”。

#include <cstdio>
#include <mutex>
#include <thread>
std::mutex mtx1;
int main() {
    if (mtx1.try_lock())
        printf("succeed\n");
    else
        printf("failed\n");
    std::thread t1([] { // 同一个线程不能 double-lock,也不能 double-try_lock,但没说另一个线程不可以
        if (mtx1.try_lock())
            printf("succeed\n");
        else
            printf("failed\n");
    });
    t1.join(); // join 在 unlock 前,保证 t1 里的 try_lock 是在 main 持有锁的情况下完成执行的
    mtx1.unlock();
    return 0;
}

运行结果:

succeed
failed
YZH-bot commented 1 year ago

Get,感谢指出👍