Личный опыт разработки ПО

Сборник рецептов

Многопоточность, баги и списки инициализации

Прокомментировать

Разработка многопоточных программ – задача нетривиальная. Локализовать место ошибки сложно, отладка затруднена, иногда даже возникает желание все свалить на ошибки в сторонних библиотеках и/или компилятор/операционную систему. Так делать не надо. Конечно вероятность такая есть, но она несравненно меньше того, что виновником ошибки являетесь именно вы.

Например приложение работает хорошо, ну скажем в 95 случаях из 100, но в оставшихся пяти оно может зависнуть или вылететь с segmentation fault. А может упасть на таком ассерте:

/usr/include/boost/thread/pthread/mutex.hpp:51:
	void boost::mutex::lock(): Assertion `!pthread_mutex_lock(&m)' failed. 


Посмотрим на следующий код:

#include <iostream>
#include <queue>
 
#include <boost/thread.hpp>
 
class Worker
{
public:
	Worker()
		: Shutdown_(false)
		, Thread_(boost::thread(boost::bind(&Worker::ThreadFunction, this)))
	{
	}
 
	~Worker()
	{
		{
			boost::mutex::scoped_lock lock(Mutex_);
			Shutdown_ = true;
		}
 
		Condition_.notify_one();
 
		Thread_.join();
	}
 
	void AddTask(int taskId)
	{
		{
			boost::mutex::scoped_lock lock(Mutex_);
 
			std::cout << "Adding task " << taskId << std::endl;
 
			Tasks_.push(taskId);
		}
 
		Condition_.notify_one();
	}
 
private:
	void ThreadFunction()
	{
		while (true)
		{
			boost::mutex::scoped_lock lock(Mutex_);
 
			if (!Tasks_.empty())
			{
			 std::cout << "Processing task " << Tasks_.front() << std::endl;
 
			 Tasks_.pop();
				continue;
			}
 
			if (Shutdown_)
			{
			 return;
			}
 
			Condition_.wait(lock);
		}
	}
 
private:
	bool Shutdown_;
 
	std::queue<int> Tasks_;
 
	boost::thread Thread_;
	boost::mutex Mutex_;
	boost::condition_variable Condition_;
};
 
int main(int argc, char* argv[])
{
	Worker worker;
 
	for (int i = 1; i !=6; ++i)
	{
		worker.AddTask(i);
	}
 
	return 0;
}

Здесь приведен простой класс, объект которого обрабатывает некоторые задачи в параллельном потоке. Из основного потока функцией AddTask можно добавить задачу в очередь и известить об этом параллельный поток. Поток проснувшись, возьмет из очереди задачу, обработает ее и если задач больше нет – уснет. В деструкторе, прежде чем объект будет уничтожен происходит ожидание обработки всех задач в очереди.

На первый взгляд все прозрачно, но в некоторых случаях, мы вылетим по ассерту из boost (и даже можем начать грешить на его разработчиков) или зависнем, но повторюсь – ошибка здесь наша.

Проблема в списке инициализации. Как мы помним, конструирование членов класса происходит в порядке их объявления. В нашем случае сначала будет создан объект типа boost::thread и только после него boost::mutex. Если звезды сложатся удачно, то запуск параллельного потока займет некоторое время, за которое в основном потоке будет сконструирован мютекс и программа будет работать. Если со звездами не сложиться, то поток начав работу попробует заблокировать мютекс который еще не сконструирован, со всеми вытекающими. Решение простое – изменить положение членов в списке инициализации:

boost::mutex Mutex_;
boost::condition_variable Condition_;
boost::thread Thread_;

15 ноября 2011
21:38

Рубрика: C++

Метки: , ,

Оставить комментарий

Я не робот!