问题描述
这里有一个用于演示的简单代码片段。
有人告诉我复查锁是不正确的。由于变量是非易失性的,编译器可以自由地对调用重新排序或对其进行优化(有关详细信息,请参阅codereview.stackexchange.com/a/266302/226000).
但我真的看到这样的代码片段确实在许多项目中使用。有谁能解释一下这件事吗?我用谷歌搜索了一下,和朋友们聊了聊,但我仍然找不到答案。
#include <iostream>
#include <mutex>
#include <fstream>
namespace DemoLogger
{
void InitFd()
{
if (!is_log_file_ready)
{
std::lock_guard<std::mutex> guard(log_mutex);
if (!is_log_file_ready)
{
log_stream.open("sdk.log", std::ofstream::out | std::ofstream::trunc);
is_log_file_ready = true;
}
}
}
extern static bool is_log_file_ready;
extern static std::mutex log_mutex;
extern static std::ofstream log_stream;
}
//cpp
namespace DemoLogger
{
bool is_log_file_ready{false};
std::mutex log_mutex;
std::ofstream log_stream;
}
更新:
感谢你们所有人。InitFd()
确实有更好的实现,但这只是一个简单的演示,我真的想知道的是双重检查锁是否有任何潜在的问题。
有关完整代码片段,请参阅https://codereview.stackexchange.com/questions/266282/c-logger-by-template。
推荐答案
双重检查锁不正确,因为is_log_file_ready
是纯bool
,并且该标志可以被多个线程访问,其中一个线程是编写器,这是竞争
简单的解决方法是更改声明:
std::atomic<bool> is_log_file_ready{false};
然后可以进一步放宽is_log_file_ready
:
void InitFd()
{
if (!is_log_file_ready.load(std::memory_order_acquire))
{
std::lock_guard<std::mutex> guard(log_mutex);
if (!is_log_file_ready.load(std::memory_order_relaxed))
{
log_stream.open("sdk.log", std::ofstream::out | std::ofstream::trunc);
is_log_file_ready.store(true, std::memory_order_release);
}
}
}
但一般来说,应避免双重检查锁定,除非在低级别实现中。
正如Arthur P.Golubev所建议的,C++提供了完成此任务的原语,如std::call_once
更新:
这里有一个示例,它显示了比赛可能导致的问题之一。
#include <thread>
#include <atomic>
using namespace std::literals::chrono_literals;
int main()
{
int flag {0}; // wrong !
std::thread t{[&] { while (!flag); }};
std::this_thread::sleep_for(20ms);
flag = 1;
t.join();
}
设置sleep
是为了给线程一些时间进行初始化。
此程序应该立即返回,但经过完全优化编译后-O3
可能不会。这是由有效的编译器转换引起的,该转换将While循环更改为如下所示:
if (flag) return; while(1);
如果标志为(仍然)零,这将永远运行(将flag
类型更改为std::atomic<int>
将解决此问题)。
这只是未定义行为的影响之一,编译器甚至不必将对flag
的更改提交到内存。
在竞争或错误设置(或丢失)障碍的情况下,操作也可能被重新排序,导致不想要的影响,但这些情况在X86
上发生的可能性较小,因为它通常是一个比较弱的体系结构更容错的平台(尽管重新排序影响X86
)
这篇关于C++的双重检查锁有什么潜在的问题吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!