C++ 11 中线程之间的共享文件日志记录

Shared file logging between threads in C++ 11

最近我开始学习C++ 11。我在college.I来自另一个生态系统(Web开发)时只学习了很短的一段时间C/C++所以你可以想象我是 C++ 的新手。

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

非常感谢您抽出宝贵的时间,下面是源代码(目前出于学习目的,所有内容都在 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++ 标准库中没有任何东西是线程安全的。 here.

给出了在多线程环境中使用 ofstream 的一个很好的答案

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

My First question and request would be to point out any bad practices / mistakes that I have overlooked (although the code works with 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();

现在您创建所有线程,然后等待所有线程。不一一列举了。

Secondly and this is what is my main concern is that I'm not closing the file handle, and I'm not sure If that causes any issues. If it does when and how would be the most appropriate way to close it?

你想什么时候关闭ose?每次写完后?那将是一项多余的 OS 工作,没有任何实际好处。该文件支持在整个程序的生命周期内打开。osed。因此根本没有理由手动 close 它。正常退出 std::ofstream 将调用其 clos 文件的析构函数。在非正常退出时,os 无论如何都会关闭 ose 所有剩余的句柄。

刷新文件缓冲区(possibly after each write?)会有所帮助。

Lastly and correct me if I'm wrong I don't want to "pause" a thread while another thread is writing. I'm writing line by line each time. Is there any case that the output messes up at some point?

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

有许多不同的同步机制。但所有这些要么是无锁的,要么是基于锁的(或者 pos 可能是混合)。无论如何,记录器 class 中的简单 std::mutex(基本的基于锁的同步)应该没问题。

您好,欢迎来到社区!

对代码的一些评论,以及一些最重要的一般提示。

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

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

  3. 如果您没有特定的内存管理要求,请不要使用智能指针,在这种情况下,对堆栈分配记录器的引用就可以了(记录器可能会在程序,因此不需要显式内存管理)。在 C++ 中,您尝试尽可能多地使用堆栈,动态内存分配在许多方面都很慢,并且共享指针会引入开销(唯一指针是零成本抽象)。

  4. for循环中的join可能不是你想要的,它会等待之前生成的线程并在完成后生成另一个线程。如果你想要并行性,你需要另一个 for 循环来连接,但首选方法是使用 std::for_each(begin(pool), end(pool), [](auto& thread) { thread.join(); }) 或类似的东西。

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

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

第一个大错误是说 "it works with MSVC, I see no garbage",更重要的是它只工作因为你的测试代码被破坏了(好吧它没有被破坏,但它不是并发的,所以 当然 它工作正常)。

但即使代码是并发的,说 "I don't see anything wrong" 也是一个可怕的错误。除非您发现错误,否则多线程代码永远不会正确,除非被证明是正确的,否则它是不正确的。

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

在某些操作系统下,您可以 "cheat" 无需同步就可以离开,因为这些操作系统提供具有原子性保证的系统调用(例如 writev)。然而不是你想的那样,它确实是重量级同步,只是你看不到。

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

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

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

也就是说,有许多免费且随时可用的日志记录库(例如 spdlog)可以很好地完成这项工作。真的没有太多理由重新发明这个特殊的轮子。