提问人:Ronen 提问时间:1/27/2022 更新时间:1/30/2022 访问量:555
std::condition_variable 的布尔谓词是否应该在 C++ 中是可变的?
Should a boolean predicate for std::condition_variable be volatile in C++?
问:
我听到了这么多相互矛盾的答案,现在我不知道该怎么想。 公认的知识是,为了在 C++ 中以线程安全的方式共享内存,需要将 volatile 与 std::mutex 一起使用。
基于这种理解,我一直在编写这样的代码:
volatile bool ready = false;
std::condition_variable cv;
std::mutex mtx;
std::unique_lock<std::mutex> lckr{ mtx };
cv.wait(lckr, [&ready]() -> bool { return ready; });
但后来我在 CppCon 上看到了 Chandler Carruth 的演讲,他说(作为旁注)在这种情况下不需要 volatile,我基本上不应该使用 volatile。
然后我在 Stack Overflow 中看到了其他答案,说永远不应该使用挥发性,它不够好,它根本不能保证原子性。
钱德勒·卡鲁斯(Chandler Carruth)是对的吗?我们俩都错了吗?
现在我有 3 个选择:
- 必须使用 volatile 或 std::atomic
- 任何布尔值都可以
- 必须是 std::atomic
我想知道 C++14 ISO 标准是否允许我编写这样的代码:
#include <condition_variable>
#include <mutex>
#include <iostream>
#include <future>
#include <functional>
struct sync_t
{
std::condition_variable cv;
std::mutex mtx;
bool ready{ false };
};
static void threaded_func(sync_t& sync)
{
std::lock_guard<std::mutex> lckr{ sync.mtx };
sync.ready = true;
std::cout << "Waking up main thread" << std::endl;
sync.cv.notify_one();
}
int main()
{
sync_t sync;
{
std::unique_lock<std::mutex> lckr{ sync.mtx };
sync.ready = false;
std::future<void> thread =
std::async(std::launch::async, threaded_func, std::ref(sync));
std::cout << "Preparing to sleep" << std::endl;
sync.cv.wait(lckr, [&sync]() -> bool { return sync.ready; });
thread.get();
}
std::cout << "Done program execution" << std::endl;
return 0;
}
当我成功时会发生什么:
volatile bool ready{ false };
当我成功时会发生什么:
std::atomic<bool> ready{ false };
答:
不,volatile 是令人困惑的关键字,但它与并发性无关,不像 C# 或 Java 那样保证顺序一致性。这里只是提示编译器不要优化变量。
评论
限定符对从不同线程访问对象没有必需的影响 - 它仅保证编译器不会优化单个线程中修改的副作用。来自 cppreference (粗体强调我的):volatile
- volatile object - 类型为 volatile-qualified 的对象,或者是 volatile 对象的子对象,或者是 常量-易失性对象。每次访问(读或写操作,成员 函数调用等)通过 GLvalue 表达式 挥发性限定类型被视为 优化目的(即,在 执行时,无法优化或重新排序易失性访问 具有另一个可见的副作用,该副作用在之前排序 - 或 在易失性访问之后排序。这使得易失性对象 适合与信号处理程序通信,但不适用于 另一个执行线程,参见 std::memory_order)。任何尝试 通过非易失性类型的 glvalue 引用易失性对象 (例如,通过引用或指向非易失性类型的指针)导致 未定义的行为。
为了防止在从多个线程访问对象时出现未定义的行为,应使用对象。同样,来自 cppreference:std::atomic
std::atomic 模板的每个实例化和完全专用化 定义原子类型。如果一个线程写入原子对象,而 另一个线程从中读取,行为定义明确(参见内存 模型,了解有关数据争用的详细信息)。
此外,对原子对象的访问可以建立线程间 同步和排序非原子内存访问,如 std::memory_order。
评论
std::atomic
bool ready
std::mutex
lock_guard
volatile
volatile 只需告诉编译器,即使您不知道是谁,有人可能会更改此值,例如,它可能是某些硬件、信号甚至其他线程。 一个著名的例子是:
bool flag
foo()
{
flag = true;
while(flag)
{
}
}
优化的编译器将看到 flag 为 true,并且由于它只是一个普通的全局变量,它可以假设除了当前线程之外没有人可以更改它,因此编译器可以假设 flag 始终为 true,因此将 切换到 to 以形成无限循环。
但是,如果将标志变量声明为 volatile,则编译器不能假定只有当前线程触及此值,因此代码将保持不变。while(flag)
while(1)
现在对于您的问题,volatile 将帮助我们通知编译器其他人可能会使用此值,但这对于多线程来说是不够的,因为它不会阻止数据竞争,这是 C 语言中未定义的行为,因此我们需要将 bool 标志声明为 std::atomic。
请注意,编译器从 std::atomic 声明中理解的一件事是另一个线程可能会使用此值,因此我们无法进行上述优化。
对于您的示例,正如我们所解释的,volatile 是不够的,但您也不需要 std::atomic,因为您有一个锁,所以如果您的锁工作正常,那么当您在关键部分内时,没有其他线程可能会触及该值,因此 std::atomic 是多余的。
std::atomic主要用于关键部分,当所有关键部分都是因为原子操作时,所以我们可以使用std::atomic来代替一个较慢的锁(情况并非总是如此,这取决于流程)。
评论
自从我问了这个问题以来,我现在知道了更多。 答案是 Chandler Carruth 是正确的,一个常规的布尔值(带有 std::mutex)就足够了。不需要原子,也不需要挥发性。 “volatile”只应在处理信号处理程序时使用,如下所示:
volatile std::sig_atomic_t
与流行的看法相反,当您使用 std::mutex 时,C++ 编译不允许只是“优化”从布尔值中读取。这是因为锁定互斥锁就像一个“栅栏”,在锁定互斥锁后,编译器必须假设变量可能已经更改。编译器仍然可以优化它可以证明没有更改的局部变量,但是在我使用布尔值作为谓词的示例中,我通过引用另一个函数来发送布尔值:
std::future<void> thread =
std::async(std::launch::async, threaded_func, std::ref(sync));
布尔值存在于“sync”内部,因此现在不允许编译器假定该值保持不变。编译仍然可以将布尔值存储在寄存器中,但是当我锁定 std::mutex 时,它将被迫重新加载该值,因为它可能已经更改。 当然(根据标准)函数 std::condition_variable::wait 在返回时保持 std::mutex 锁定,因此当 std::mutex 被锁定时,它将始终检查布尔谓词,认为整个事情是安全的。
总而言之:多线程永远不需要 volatile。std::mutex 就足够了。
我的问题的答案是,我用普通布尔值编写的代码是安全的,所有三个选项也是安全的。使用 volatile 是安全的,使用 std::atomic 是安全的。但是在这种情况下,使用常规布尔值是最正确和最有效的。 事实上,如果你有一个锁(std::mutex),你就永远不需要std::atomic。 需要注意的是,如果我每次从谓词读取时都没有努力使用 std::mutex,那么 std::atomic 将是必需的。
我从这里对我的问题给出的各种答案中获取了这些知识,并且我还在 Compiler Explorer 中使用 Clang13 对其进行了测试。看到C++14标准的证明会很有趣。
评论
volatile
bool
condition_variable
volatile
volatile
volatile