0

最近我开始学习 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!

到现在都没看到垃圾。。。

4

4 回答 4

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(基于锁的基本同步)应该没问题。

于 2017-11-23T12:00:21.820 回答
0

第一个巨大的错误是说“它可以与 MSVC 一起使用,我看不到垃圾”,甚至因为它仅在您的测试代码被破坏时才有效(它没有被破坏,但它不是并发的,所以它当然可以正常工作)。

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

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

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

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

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

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

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

于 2017-11-23T14:59:54.627 回答
0

您好,欢迎来到社区!

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

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

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

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

  4. for 循环中的连接可能不是您想要的,它会等待先前产生的线程并在完成后产生另一个线程。如果您想要并行性,您需要另一个用于连接的 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++ 的最大优势之一。确保你理解堆栈、构造函数和析构函数是如何工作的。

于 2017-11-23T12:06:03.577 回答
-1

第一个问题是主观的,所以其他人会想给出建议,但我看不出有什么可怕的。

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

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

于 2017-11-23T11:49:19.803 回答