C++的双重检查锁有什么潜在的问题吗? [英] Is there any potential problem with double-check lock for C++?

查看:40
本文介绍了C++的双重检查锁有什么潜在的问题吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这里有一个用于演示的简单代码片段。

有人告诉我复查锁是不正确的。由于变量是非易失性的,编译器可以自由地对调用重新排序或对其进行优化(有关详细信息,请参阅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++的双重检查锁有什么潜在的问题吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆