c++ - C++ 的双重检查锁是否有任何潜在问题?

标签 c++ multithreading locking mutex atomic

这是一个简单的演示代码片段。

有人告诉我复查锁不正确。由于变量是非 volatile 的,编译器可以自由地重新排序调用或优化它们(详情,请参阅 codereview.stackexchange.com/a/266302/226000)。

但我确实看到确实在许多项目中使用了这样的代码片段。有人可以对此事有所了解吗?我google了一下,和 friend 讨论过,还是没找到答案。

#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 (仍然)为零,这将永远运行(将 flag 类型更改为 std::atomic<int> 将解决此问题)。

这只是未定义行为的影响之一,编译器甚至不必将更改提交到 flag内存。

在比赛中,或错误设置(或丢失)障碍时,操作也可能被重新排序,从而导致不必要的影响,但这些不太可能发生在 X86因为它通常是一个比较弱架构更宽容的平台(尽管重新排序会影响 X86 上的 do exist)

关于c++ - C++ 的双重检查锁是否有任何潜在问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68973582/

相关文章:

locking - SVN : how to lock a file so that no one can modifiy it?

c++ - 离开功能后丢失一些数据

c# - 为什么 Cdecl 调用在 "standard"P/Invoke 约定中经常不匹配?

c# - ASP.NET Web API 异步响应和写入

java - 寻找并证明最佳的端口/ socket /线程比

java - 如何促进Java中服务器线程和多个客户端线程之间的通信

mysql - 通过显式排序避免死锁

java - AtomicBoolean 锁在哪里?

c++ - 使用 QT creator,如何创建新屏幕?

c++ - 在 C++ 中的 extern "C"期间是否只允许 C 定义的数据类型?