最近我开始学习C ++ 11.我在大学时只学习了很短一段时间的C / C ++。我来自另一个生态系统(web开发),所以你可以想象我是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!
到目前为止,我看不到任何垃圾......
答案 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)
您好,欢迎来到社区!
关于代码的一些评论,以及一些基本提示。
如果您不是必须的话,请不要使用原生数组。
消除本机std::thread[]
数组并将其替换为std::array
将允许您执行基于范围的循环,这是在C ++中迭代事物的首选方式。 std::vector
也可以使用,因为您必须生成thredas(您可以将std::generate
与std::back_inserter
结合使用)
如果您没有特定的内存管理要求,请不要使用智能指针,在这种情况下,对堆栈分配的记录器的引用会很好(记录器可能会在程序的持续时间内存在,因此不需要显式内存管理)。在C ++中,您尝试尽可能多地使用堆栈,动态内存分配在很多方面都很慢,共享指针引入了开销(唯一指针是零成本抽象)。
for循环中的连接可能不是您想要的,它将等待先前生成的线程并在完成后生成另一个。如果你想要并行性,你需要另一个用于连接的循环,但是优先使用std::for_each(begin(pool), end(pool), [](auto& thread) { thread.join(); })
或类似的东西。
使用C ++核心指南和最近的C ++标准(C ++ 17是当前的),C ++ 11已经过时了,你可能想学习现代的东西,而不是学习如何编写遗留代码。 http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
C ++不是java,尽可能多地使用堆栈 - 这是使用C ++的最大优势之一。确保你理解堆栈,构造函数和析构函数是如何工作的。
答案 2 :(得分:0)
第一个大错误是说“它适用于MSVC,我看不到垃圾”,甚至更多,因为它只能起作用,因为你的测试代码被破坏了(好吧它没有被破坏,但它不是并发的,所以当然它工作正常。)
但即使代码是并发的,说“我没有看到任何错误”是一个可怕的错误。除非您发现错误,否则多线程代码永远不会正确,除非证明是正确的,否则它是不正确的。
如果你想要正确性,那么在另一个线程写入时不阻塞(“暂停”)一个线程的目标是不可实现的,至少如果它们同时写入同一个描述符。您必须正确同步(以您喜欢的方式调用它,并使用您喜欢的任何方法),否则行为将不正确。或者更糟糕的是,只要你看一下它就会看起来正确,而且当你最重要的客户将它用于一个价值数百万美元的项目时,它会在六个月后出现问题。
在某些操作系统下,您可以“欺骗”并在没有同步的情况下离开,因为这些系统提供具有原子性保证的系统调用(例如writev
)。然而,这不是你可能想到的,它确实是重量级的同步,只是你没有看到它。
比使用互斥锁或使用原子写入更好(更有效)的策略可能是有一个单个使用者线程写入磁盘,并将日志任务从如何推送到并发队列许多生产者线程你喜欢。这对于您不想阻止的线程具有最小延迟,并阻塞您不关心的位置。另外,您可以将多个小写合并为一个。
关闭或不关闭文件似乎不是问题。毕竟,当程序退出时,无论如何文件都会关闭。是的,除了有三层缓存(实际上有四层,如果你算上物理磁盘的缓存),其中两层在你的应用程序中,另一层在操作系统中。
当数据至少进入OS缓冲区时,除非意外断电,否则一切都很好。对于其他两个级别的缓存不是这样! 如果您的进程意外死亡,其内存将被释放,其中包括iostream中缓存的任何内容以及CRT中缓存的任何内容。因此,如果您需要任何可靠性,您将需要定期冲洗(这是昂贵的),或使用不同的策略。文件映射可能是一种策略,因为无论您复制到映射中的是自动(按照定义)在操作系统的缓冲区内,除非电源出现故障或计算机爆炸,否则将写入磁盘。 / p>
话虽如此,仍有许多免费且易于使用的日志库(例如spdlog)可以很好地完成这项工作。真正没有理由重新发明这个特定的轮子。
答案 3 :(得分:-1)