【问题标题】:Shared file logging between threads in C++ 11 [duplicate]C ++ 11中线程之间的共享文件日志记录[重复]
【发布时间】:2018-05-07 08:46:52
【问题描述】:

最近我开始学习 C++ 11。我在大学时只学习了一小段时间 C/C++。我来自另一个生态系统(Web 开发),所以你可以想象我对 C++ 还比较陌生。

目前我正在研究线程以及如何使用单个编写器(文件句柄)从多个线程完成日志记录。所以我根据教程和阅读各种文章编写了以下代码。

  • 我的第一个问题和要求是指出我忽略的任何不良做法/错误(尽管代码适用于 VC 2015)。
  • 其次,我主要担心的是我没有关闭文件句柄,我不确定这是否会导致任何问题。如果确实如此,何时以及如何关闭它是最合适的方式?
  • 最后,如果我错了,请纠正我,我不想在另一个线程正在写入时“暂停”一个线程。我每次都在一行一行地写。是否有任何情况下输出会在某些时候出现混乱?

非常感谢您抽出宝贵时间,下面是源代码(目前出于学习目的,所有内容都在 main.cpp 内)。

#include <iostream>
#include <fstream>
#include <thread>
#include <string>

static const int THREADS_NUM = 8;

class Logger
{

  public:
    Logger(const std::string &path) : filePath(path)
    {
        this->logFile.open(this->filePath);
    }

    void write(const std::string &data)
    {
        this->logFile << data;
    }

  private:
    std::ofstream logFile;
    std::string filePath;

};

void spawnThread(int tid, std::shared_ptr<Logger> &logger)
{

    std::cout << "Thread " + std::to_string(tid) + " started" << std::endl;

    logger->write("Thread " + std::to_string(tid) + " was here!\n");

};

int main()
{

    std::cout << "Master started" << std::endl;
    std::thread threadPool[THREADS_NUM];

    auto logger = std::make_shared<Logger>("test.log");

    for (int i = 0; i < THREADS_NUM; ++i)
    {
        threadPool[i] = std::thread(spawnThread, i, logger);
        threadPool[i].join();
    }

    return 0;
}

PS1:在这种情况下,将始终只打开 1 个文件句柄供线程记录数据。

PS2:理想情况下,文件句柄应该在程序退出之前关闭...应该在 Logger 析构函数中完成吗?

更新

1000线程的当前输出如下:

Thread 0 was here!
Thread 1 was here!
Thread 2 was here!
Thread 3 was here!
.
.
.
.
Thread 995 was here!
Thread 996 was here!
Thread 997 was here!
Thread 998 was here!
Thread 999 was here!

到目前为止我还没有看到任何垃圾......

【问题讨论】:

  • 正如许多人(包括我)所指出的那样,您的日志是按顺序运行的,而不是并行运行的,因为您在生成每个线程后立即加入。
  • 是的,我错过了,你们是对的 :)

标签: c++ multithreading c++11 make-shared


【解决方案1】:

我的第一个问题和要求是指出我忽略的任何不良做法/错误(尽管代码适用于 VC 2015)。

主观,但代码对我来说看起来不错。尽管您没有同步线程(记录器中的一些 std::mutex 可以解决问题)。

还要注意:

std::thread threadPool[THREADS_NUM];

auto logger = std::make_shared<Logger>("test.log");

for (int i = 0; i < THREADS_NUM; ++i)
{
    threadPool[i] = std::thread(spawnThread, i, logger);
    threadPool[i].join();
}

毫无意义。您创建一个线程,加入它,然后创建一个新线程。我认为这就是您要寻找的:

std::vector<std::thread> threadPool;

auto logger = std::make_shared<Logger>("test.log");

// create all threads
for (int i = 0; i < THREADS_NUM; ++i)
    threadPool.emplace_back(spawnThread, i, logger);
// after all are created join them
for (auto& th: threadPool)
    th.join();

现在您创建所有线程,然后等待所有线程。不是一一的。

其次,我主要担心的是我没有关闭文件句柄,我不确定这是否会导致任何问题。如果确实如此,何时以及如何关闭它是最合适的方式?

你想什么时候关闭它?每次写完之后?那将是一个多余的操作系统工作,没有真正的好处。该文件应该在整个程序的生命周期中打开。因此,根本没有理由手动关闭它。优雅退出std::ofstream 将调用它的析构函数来关闭文件。在非正常退出时,操作系统无论如何都会关闭所有剩余的句柄。

刷新文件的缓冲区(可能在每次写入之后?)会有所帮助。

最后,如果我错了,请纠正我,我不想在另一个线程正在写作时“暂停”一个线程。我每次都在一行一行地写。是否有任何情况下输出会在某些时候出现混乱?

是的,当然。您没有同步对文件的写入,输出可能是垃圾。您实际上可以轻松地自己检查:生成 10000 个线程并运行代码。您很可能会收到损坏的文件。

有许多不同的同步机制。但它们都是无锁或基于锁的(或者可能是混合的)。无论如何,记录器类中的简单std::mutex(基于锁的基本同步)应该没问题。

【讨论】:

  • 感谢您的回复,我编辑了我的问题。该文件应在整个程序的生命周期内保持打开状态。
  • @Syd 你不需要做任何特别的事情。 std::ofstream 析构函数为你关闭句柄。当没有更多对logger 的引用时,即在程序退出时,它将触发。
  • 好的,我也尝试了 1000 个线程,但看起来还不错。
  • @Syd 那是因为您的原始代码不是多线程的。查看我的更新。
  • 哦,我没有意识到我在做关于线程的事情!你是对的,现在每个日志行看起来都是随机的(意味着它被附加到文件中的顺序)
【解决方案2】:

第一个大错误是说“它与 MSVC 一起工作,我看不到垃圾”,更何况它只是因为你的测试代码被破坏而工作(好吧它没有被破坏,但它不是并发的,所以 当然 效果很好)。

但即使代码是并发的,说“我没有看到任何错误”也是一个可怕的错误。多线程代码永远不会正确,除非您发现有问题,否则它是不正确的,除非被证明是正确的。

如果您想要正确性,至少在它们同时写入同一个描述符的情况下,不阻塞(“暂停”)一个线程而另一个线程正在写入的目标是无法实现的。您必须正确同步(以您喜欢的方式调用它,并使用您喜欢的任何方法),否则行为将不正确。或者更糟的是,只要您查看它,它就会看起来是正确的,而六个月后,当您最重要的客户将它用于数百万美元的项目时,它就会表现出错误的行为。

在某些操作系统下,您可以“作弊”并在不同步的情况下逃脱,因为这些操作系统提供具有原子性保证的系统调用(例如 writev)。然而这并不是你想的那样,它确实是重量级的同步,只是你没有看到。

比使用互斥锁或原子写入更好(更有效)的策略可能是拥有一个单个消费者线程来写入磁盘,并将日志任务推送到并发队列中许多你喜欢的生产者线程。对于您不想阻塞的线程和在您不关心的地方阻塞,这具有最小的延迟。此外,您可以将几个小的写入合并为一个。

关闭或不关闭文件似乎不是问题。毕竟,当程序退出时,文件无论如何都会关闭。嗯,是的,除了,有三层缓存(如果算上物理磁盘的缓存,实际上是四层),其中两层在您的应用程序中,一层在操作系统中。

当数据至少进入操作系统缓冲区时,一切都很好,除非电源意外中断。其他两级缓存则不然!
如果您的进程意外终止,它的内存将被释放,其中包括缓存在 iostream 中的任何内容以及缓存在 CRT 中的任何内容。因此,如果您需要任何程度的可靠性,您要么必须定期冲洗(这很昂贵),要么使用不同的策略。文件映射可能是这样一种策略,因为您复制到映射中的任何内容都会自动(根据定义)在操作系统的缓冲区中,除非电源故障或计算机爆炸,否则它将写入磁盘。

话虽如此,有几十个免费且现成的日志库(例如 spdlog)可以很好地完成这项工作。真的没有太多理由重新发明这个特殊的轮子。

【讨论】:

    【解决方案3】:

    您好,欢迎来到社区!

    关于代码的一些 cmets,以及最重要的一些一般性提示。

    1. 如果不是绝对必要,请不要使用本机数组。

    2. 消除本机 std::thread[] 数组并将其替换为 std::array 将允许您执行基于范围的 for 循环,这是在 C++ 中迭代事物的首选方式。 std::vector 也可以工作,因为您必须生成线程(您可以将 std::generate 与 std::back_inserter 结合使用)

    3. 1234563不需要显式的内存管理)。在 C++ 中,您尝试尽可能多地使用堆栈,动态内存分配在许多方面都很慢,并且共享指针会引入开销(唯一指针是零成本抽象)。
    4. for 循环中的连接可能不是您想要的,它会等待先前产生的线程,并在完成后产生另一个线程。如果您想要并行性,您需要另一个用于连接的 for 循环,但首选方法是使用 std::for_each(begin(pool), end(pool), [](auto&amp; thread) { thread.join(); }) 或类似的东西。

    5. 使用 C++ 核心指南和最近的 C++ 标准(C++17 是当前标准),C++11 已经过时了,您可能想学习现代的东西而不是学习如何编写遗留代码。 http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines

    6. C++ 不是 java,尽可能使用堆栈——这是使用 C++ 的最大优势之一。确保您了解堆栈、构造函数和析构函数的工作原理。

    【讨论】:

    • 您可以将范围用于原生数组。
    【解决方案4】:

    第一个问题是主观的,所以其他人想给出建议,但我没有看到任何糟糕的事情。

    C++ 标准库中没有任何东西是线程安全的,除了一些罕见的情况。在多线程环境中使用ofstream 的一个很好的答案是here。

    不关闭文件确实是个问题。你必须熟悉RAII,因为它是首先要学习的东西之一。 Detonar 的回答是一个很好的建议。

    【讨论】:

    • 为什么不关闭文件会成为问题?无论如何,操作系统都会在退出时关闭所有剩余的文件句柄。此外,由于 RAII 不会保护您免受 SIGKILL 之类的影响,我根本没有理由实施它。
    • std::ofstream 会在Logger 的成员在Logger 的销毁时自动关闭文件或您的意思是什么?
    • 当然,ofstream 的析构函数会调用close()。我的观点是,了解 RAII 比找到使用 ofstream 的解决方案更有益。
    猜你喜欢
    • 1970-01-01
    • 2014-05-03
    • 2010-09-22
    • 1970-01-01
    • 2017-03-02
    • 2021-07-14
    • 1970-01-01
    • 2012-12-29
    相关资源
    最近更新 更多