C ++ 11

时间:2017-11-23 11:36:15

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

最近我开始学习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 个答案:

答案 0 :(得分:1)

  

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

主观,但代码对我来说很好。虽然你没有同步线程(记录器中的一些List<StructLog> All = new List<StructLog>();会起作用)。

另请注意:

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(); 将调用其析构函数来关闭文件。在非优雅的退出时,操作系统将关闭所有剩余的手柄。

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

  

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

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

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

答案 1 :(得分:0)

您好,欢迎来到社区!

关于代码的一些评论,以及一些基本提示。

  1. 如果您不是必须的话,请不要使用原生数组。

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

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

  4. 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 ++的最大优势之一。确保你理解堆栈,构造函数和析构函数是如何工作的。

答案 2 :(得分:0)

第一个大错误是说“它适用于MSVC,我看不到垃圾”,甚至更多,因为它只能起作用,因为你的测试代码被破坏了(好吧它没有被破坏,但它不是并发的,所以当然它工作正常。)

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

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

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

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

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

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

话虽如此,仍有许多免费且易于使用的日志库(例如spdlog)可以很好地完成这项工作。真正没有理由重新发明这个特定的轮子。

答案 3 :(得分:-1)

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

除了一些罕见的情况,C ++标准库中的任何内容都不是线程安全的。在多线程环境中使用ofstream的好答案是here

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