我编写了一个用于解析日志文件的多线程应用程序。它基于http://drdobbs.com/cpp/184401518?pgno=5的互斥缓冲区示例。
我们的想法是拥有一个缓冲类,它具有将项目放入缓冲区并从中取出项目的功能。使用条件处理读取和写入线程的同步。当缓冲区未满时,新项目将写入缓冲区,并且当它不为空时,将从中读取项目。否则线程会等待。
该示例使用固定数量的项目进行处理,因此我更改了读取线程,当文件中有输入并且处理线程在输入时或缓冲区中有剩余项目时运行。
我的问题是,如果我使用1个读取和1个处理线程,一切都很好并且稳定。当我添加另一个处理线程时,性能会有很大的提升,即使经过10,000次测试,它仍然保持稳定。
现在,当我添加另一个处理器线程(1次读取,3次处理)时,程序似乎会定期挂起(死锁?)(但不是每次都挂起)并且正在等待缓冲区填充或变空。
为什么2个线程做同样的事情同步稳定,其中3个崩溃?
我是C ++线程的新手,所以也许你们中任何一位经验丰富的编码员都知道会导致这种行为的原因是什么?
这是我的代码:
缓冲类:
#include "StringBuffer.h"
void StringBuffer::put(string str)
{
scoped_lock lock(mutex);
if (full == BUF_SIZE)
{
{
//boost::mutex::scoped_lock lock(io_mutex);
//std::cout << "Buffer is full. Waiting..." << std::endl;
}
while (full == BUF_SIZE)
cond.wait(lock);
}
str_buffer[p] = str;
p = (p+1) % BUF_SIZE;
++full;
cond.notify_one();
}
string StringBuffer::get()
{
scoped_lock lk(mutex);
if (full == 0)
{
{
//boost::mutex::scoped_lock lock(io_mutex);
//std::cout << "Buffer is empty. Waiting..." << std::endl;
}
while (full == 0)
cond.wait(lk);
}
string test = str_buffer[c];
c = (c+1) % BUF_SIZE;
--full;
cond.notify_one();
return test;
}
这是主要的:
Parser p;
StringBuffer buf;
Report report;
string transfer;
ifstream input;
vector <boost::regex> regs;
int proc_count = 0;
int push_count = 0;
bool pusher_done = false;
// Show filter configuration and init report by dimensioning counter vectors
void setup_report() {
for (int k = 0; k < p.filters(); k++) {
std::cout << "SID(NUM):" << k << " Name(TXT):\"" << p.name_at(k) << "\"" << " Filter(REG):\"" << p.filter_at(k) << "\"" << endl;
regs.push_back(boost::regex(p.filter_at(k)));
report.hits_filters.push_back(0);
report.names.push_back(p.name_at(k));
report.filters.push_back(p.filter_at(k));
}
}
// Read strings from sourcefiles and put them into buffer
void pusher() {
// as long as another string could be red, ...
while (input) {
// put it into buffer
buf.put(transfer);
// and get another string from source file
getline(input, transfer);
push_count++;
}
pusher_done = true;
}
// Get strings from buffer and check RegEx filters. Pass matches to report
void processor()
{
while (!pusher_done || buf.get_rest()) {
string n = buf.get();
for (unsigned sid = 0; sid < regs.size(); sid++) {
if (boost::regex_search(n, regs[sid])) report.report_hit(sid);
}
boost::mutex::scoped_lock lk(buf.count_mutex);
{
proc_count++;
}
}
}
int main(int argc, const char* argv[], char* envp[])
{
if (argc == 3)
{
// first add sourcefile from argv[1] filepath, ...
p.addSource(argv[1]);
std::cout << "Source File: *** Ok\n";
// then read configuration from argv[2] filepath, ...
p.readPipes(envp, argv[2]);
std::cout << "Configuration: *** Ok\n\n";
// and setup the Report Object.
setup_report();
// For all sourcefiles that have been parsed, ...
for (int i = 0; i < p.sources(); i++) {
input.close();
input.clear();
// open the sourcefile in a filestream.
input.open(p.source_at(i).c_str());
// check if file exist, otherwise throw error and exit
if (!input)
{
std::cout << "\nError! File not found: " << p.source_at(i);
exit(1);
}
// get start time
std::cout << "\n- started: ";
ptime start(second_clock::local_time());
cout << start << endl;
// read a first string into transfer to get the loops going
getline(input, transfer);
// create threads and pass a reference to functions
boost::thread push1(&pusher);
boost::thread proc1(&processor);
boost::thread proc2(&processor);
// start all the threads and wait for them to complete.
push1.join();
proc1.join();
proc2.join();
// calculate and output runtime and lines per second
ptime end(second_clock::local_time());
time_duration runtime = end - start;
std::cout << "- finished: " << ptime(second_clock::local_time()) << endl;
cout << "- processed lines: " << push_count << endl;
cout << "- runtime: " << to_simple_string(runtime) << endl;
float processed = push_count;
float lines_per_second = processed/runtime.total_seconds();
cout << "- lines per second: " << lines_per_second << endl;
// write report to file
report.create_filereport(); // after all threads finished write reported data to file
cout << "\nReport saved as: ./report.log\n\nBye!" << endl;
}
}
else std::cout << "Usage: \"./Speed-Extract [source][config]\"\n\n";
return 0;
}
非常感谢您的帮助。通过向输出添加一些计数器和线程ID,我发现了问题所在:
我注意到有几个线程可能仍在等待填充缓冲区。
我的处理线程在剩下尚未读取的新源字符串或缓冲区不为空时运行。这不好。
假设我有2个线程正在等待缓冲区填充。一旦读者读取一个新行(可能是最后几行),其他6个线程试图获取此行并锁定该项目,以便2个等待的线程可能甚至没有机会尝试解锁它
一旦他们检查了一条线被另一个线程占用,他们就会一直等待。阅读线程在到达eof时不通知它们然后停止。两个等待的线程永远等待。
我的阅读功能必须通知所有线程它已达到eof,因此如果缓冲区为空且文件不是EOF,则线程只能保持等待。
答案 0 :(得分:1)
作为@Martin,我在代码中看不到任何明显的问题。我唯一的想法是你可以尝试使用单独的条件变量来写入缓冲区并从中读取。就像现在一样,每次线程完成获取项目时,在get
方法中等待的其他线程也可能发出信号。
请考虑以下事项。缓冲区已满,因此写入器等待cond
信号。现在,读者清空队列,甚至没有作者发信号。这是可能的,因为它们使用相同的条件变量,并且更有可能获得更多的读者。每次阅读器从缓冲区中删除项目时,它都会调用notify_one
。这可以唤醒作家,但它也可以唤醒读者。假设所有通知最终都会唤醒读者。作者永远不会被释放。最后,所有线程都会等待一个信号并且你有一个死锁。
如果这是正确的,那么您有两种可能的解决方法:
notify_all
代替notify_one
,确保读者每次删除某个项目时都有机会。答案 1 :(得分:0)
我实际上看不到问题。
但有一点需要记住的是,仅仅因为一个线程是从一个带有信号的条件变量中释放出来的,并不意味着它开始运行。
一旦发布它必须在继续之前获取互斥锁,但是每次线程被安排运行时,其他人可能已经锁定了互斥锁(假设这些循环有多紧,这不会是一个惊喜)因此它暂停等待它下一个调度槽。这可能是你的冲突所在。
问题是将print语句放入代码中是不会有帮助的,因为print语句影响时序(它们很昂贵),因此你会得到不同的行为。一些便宜的东西,比如保持每个线程动作的计数可能足够便宜,这样它不会影响时间,但可以帮助您确定问题。 注意:仅在完成后打印结果。