【问题标题】:C++ scoped lock in loop blocks another thread循环中的 C++ 范围锁阻塞另一个线程
【发布时间】:2020-09-23 20:25:29
【问题描述】:

简单示例:

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

std::mutex lock_m;

void childTh() {
    while(true) {
        //std::this_thread::yield();
        //std::this_thread::sleep_for(std::chrono::milliseconds(1));
        std::unique_lock<std::mutex> lockChild(lock_m);

        std::cout << "childTh CPN1" << std::endl;

        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

int main(int, char**) {
    std::thread thr(childTh);

    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    std::unique_lock<std::mutex> lockMain(lock_m);

    std::cout << "MainTh CPN1" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(10));
    return 0;
}

lockMain 上的主线程阻塞并且永远不会到达“MainTh CPN1”。我希望当 childTh 到达迭代结束时主线程应该获取 lock_m,因为 lockChild 被破坏并且 lock_m 被释放。但这永远不会发生。 您能否详细描述一下为什么主线程在 childTh 再次锁定之前没有时间获取锁? 使用 sleep_for main 可以达到“MainTh CPN1”,但使用 yield 不能。 我知道 condition_variable 可以用来通知和解除阻塞另一个线程,但是可以只使用作用域锁吗?所以看起来在不同的线程中使用作用域锁是有风险的,即使是同一个锁。

【问题讨论】:

    标签: c++ multithreading


    【解决方案1】:

    childTh 中,lockChild 在迭代结束之前不会释放互斥锁。在该迭代结束后,它立即开始下一个迭代。这意味着您只有在销毁lockChild 和在下一次迭代中初始化lockChild 之间的时间。由于这基本上发生在下一条指令中,因此lockMain 基本上没有时间获得互斥锁上的锁。为了节省 CPU 周期,典型的锁定获取将在短时间内产生,这不像单条指令那么短,因此基本上没有机会 lockMain 能够锁定互斥锁,因为它必须完美计时.如果您将childTh 更改为

    void childTh() {
        while(true) {
            //std::this_thread::yield();
            //std::this_thread::sleep_for(std::chrono::milliseconds(1));
            {
                std::unique_lock<std::mutex> lockChild(lock_m);
    
                std::cout << "childTh CPN1" << std::endl;
            }
    
            std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        }
    }
    

    现在,在lockChild 释放互斥锁与在下一次迭代中重新获取互斥锁之间有 1 秒的延迟,这将允许lockMain 获取互斥锁。


    还请注意,您没有在 main 末尾的 thr 上调用 join。不这样做会导致thr 的析构函数抛出异常,这将导致您的程序不正确地终止。

    【讨论】:

    • 这只是示例代码,没有正确的程序完成等等。这是关于锁定冲突的示例。添加睡眠总是黑客。有没有合适的方法来花时间从另一个线程获取互斥锁?在实际项目中广泛使用范围锁,但在特殊情况下,它可能会导致所描述的问题。
    • @Yuri 这很可能意味着您持有锁的时间过长。锁只应保持尽可能短的时间,这有助于最大限度地减少这种情况。如果这种情况仍然发生,您可能需要考虑使用不同的同步方法,例如原子变量或条件变量。
    • IMO,这将是向 OP 介绍“饥饿”一词的好机会。您的回答解释了此示例中的 childTh() 线程如何饿死主线程。