我的等待 - 使用 std::mutex 的通知机制是否正确?

Tom*_*ica 5 c++ multithreading c++11 stdmutex

我开始使用 std::mutexes 来停止一个线程并等待另一个线程恢复它。它是这样工作的:

主题 1

// Ensures the mutex will be locked
while(myWaitMutex.try_lock());
// Locks it again to pause this thread
myWaitMutex.lock();
Run Code Online (Sandbox Code Playgroud)

主题 2

// Executed when thread 1 should resume processing:
myWaitMutex.unlock();
Run Code Online (Sandbox Code Playgroud)

但是我不确定这是否正确并且在所有平台上都可以正常工作。如果这不正确,那么在 C++11 中实现它的正确方法是什么?

Snp*_*nps 8

代码的问题

// Ensures the mutex will be locked
while(myWaitMutex.try_lock());
Run Code Online (Sandbox Code Playgroud)

.try_lock()尝试获取锁,true如果成功则返回,,代码表示“如果我们获取锁,则一次又一次地重试锁定它,直到失败”。我们永远不会“失败”,因为我们当前拥有我们正在等待的锁,因此这将是一个无限循环。另外,尝试使用std::mutex调用者已经获得锁定的 a 进行锁定是 UB,因此保证是 UB。如果不成功,.try_lock()将返回falsewhile退出循环。换句话说,这不能确保互斥锁被锁定。

确保互斥锁被锁定的正确方法很简单:

myWaitMutex.lock();
Run Code Online (Sandbox Code Playgroud)

这将导致当前线程(无限期地)阻塞,直到它可以获得锁。

接下来,另一个线程尝试解锁它没有锁定的互斥体。

// Executed when thread 1 should resume processing:
myWaitMutex.unlock();
Run Code Online (Sandbox Code Playgroud)

这不会起作用,因为它是在你还没有锁定的情况.unlock()下进行的。std::mutex

使用锁

使用互斥锁时,使用 RAII 所有权包装对象(例如std::lock_guard. 的使用模式std::mutex始终是:“锁定 -> 在临界区做某事 -> 解锁”。Astd::lock_guard将在其构造函数中锁定互斥体,并在其析构函数中解锁它。无需担心何时锁定和解锁之类的低级问题。

std::mutex m;
{
    std::lock_guard<std::mutex> lk{m};
    /* We have the lock until we exit scope. */
} // Here 'lk' is destroyed and will release lock.
Run Code Online (Sandbox Code Playgroud)

简单的锁可能不是完成这项工作的最佳工具

如果您想要的是能够向线程发出唤醒信号,那么可以使用wait 和 notification结构std::condition_variable。允许std::condition_variable任何调用者向等待线程发送信号,而无需持有任何锁

#include <atomic>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

using namespace std::literals;

int main() {
    std::mutex m;
    std::condition_variable cond;

    std::thread t{[&] {
        std::cout << "Entering sleep..." << std::endl;
        std::unique_lock<std::mutex> lk{m};
        cond.wait(lk); // Will block until 'cond' is notified.
        std::cout << "Thread is awake!" << std::endl;
    }};

    std::this_thread::sleep_for(3s);

    cond.notify_all(); // Notify all waiting threads.

    t.join(); // Remember to join thread before exit.
}
Run Code Online (Sandbox Code Playgroud)

然而,让事情变得更加复杂的是,有一种称为虚假唤醒的东西,这意味着任何等待的线程都可能因未知原因随时唤醒。这是大多数系统上的事实,并且与线程调度的内部工作有关。另外,我们可能需要检查在处理并发时是否确实需要等待。例如,如果通知线程恰好在我们开始等待之前发出通知,那么我们可能会永远等待,除非我们有办法首先检查这一点。

为了处理这个问题,我们需要添加一个 while 循环和一个谓词来告诉我们何时需要等待以及何时完成等待。

int main() {
    std::mutex m;
    std::condition_variable cond;
    bool done = false; // Flag for indicating when done waiting.

    std::thread t{[&] {
        std::cout << "Entering sleep..." << std::endl;
        std::unique_lock<std::mutex> lk{m};
        while (!done) { // Wait inside loop to handle spurious wakeups etc.
            cond.wait(lk);
        }
        std::cout << "Thread is awake!" << std::endl;
    }};

    std::this_thread::sleep_for(3s);

    { // Aquire lock to avoid data race on 'done'.
        std::lock_guard<std::mutex> lk{m};
        done = true; // Set 'done' to true before notifying.
    }
    cond.notify_all();

    t.join();
}
Run Code Online (Sandbox Code Playgroud)

还有其他原因说明为什么在循环内等待并使用谓词(例如 @ David Schwartz的评论中提到的“窃取的唤醒”)是个好主意。