Правило: при использовании boost::bind
будьте предельно осторожны при передаче аргументов
в виде boost::shared_ptr
(как и других типов умных указателей): по умолчанию используется
семантика копирования, поэтому в полученном функторе будет храниться копия вашего указателя с
увеличенным счётчиком ссылок. Данный факт, вкупе с использованием совместно с boost::thread
,
может стать иточником утечки ресурсов.
Ниже более детально.
На днях выстрелил себе в ногу: словил утечку файловых дескрипторов и отказ в обслуживании при сетевых подключениях. Дальнейшие изыскания показали, что не все объекты корректно уничтожаются, при перезапуске некоторых подсистем, точнее вообще не уничтожаются, а так и продолжают висеть и занимать память.
Ниже код, воспроизводящий данный самострел:
#include <iostream>
#include <list>
#include <boost/smart_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
using namespace std;
struct MyStruct
{
MyStruct()
{
clog << "ctor" << endl;
}
~MyStruct()
{
clog << "dtor" << endl;
}
};
typedef boost::shared_ptr<MyStruct> MyStructPtr;
typedef boost::weak_ptr<MyStruct> MyStructWPtr;
void fooThread(MyStructPtr ptr)
{
// (4)
MyStructWPtr weak = ptr; // create weak pointer
ptr.reset(); // release ptr (5)
while (!weak.expired())
{
boost::this_thread::sleep(boost::posix_time::seconds(1));
cout << "tick!" << endl;
}
clog << "fooThread exit" << endl;
}
struct Foo
{
list<MyStructPtr> ptrList;
boost::thread thread;
void start()
{
MyStructPtr ptr(new MyStruct); // (0)
ptrList.push_back(ptr); // (1)
thread = boost::thread(boost::bind(fooThread, ptr)); // (2)
}
void stop()
{
ptrList.clear(); // (3)
}
void join()
{
thread.join();
}
};
int main()
{
Foo foo;
foo.start();
boost::this_thread::sleep(boost::posix_time::seconds(5));
foo.stop();
foo.join();
return 0;
}
Практическая польза данного листинга нулевая. Но если посмотреть на него вниматерльно и подумать:
- Сначала мы создаём объект типа
Foo
затем вызываем методstart()
в котором создаём умный указательptr
(точка (0)), здесь у нас счётчик ссылок равен 1. Затем мы добавляем его в списокptrList
(точка (1)), здесь счётчик ссылок у нас увеличивается на 1 и становится равным 2, понятно почему так происходит - в списке сохраняется копия объета (используется копирующий конструкторshared_ptr
). В точке (2) создаётся нить, которой, в качестве параметра передаётся (при помощиbind
) наш умный указатель, логично думаем, раз в функцию потока передаём его по значению, то ещё один раз задействуется копирующий конструкторshared_ptr
и счётчик ссылок становится равным 3 в точке (4). В точке (5) говорим, что мы прекратили работать с этой копией указателя, тем самым счётчик ссылок уменьшился на 1 и стал равен 2 (пренебрегаем тем, что код может выполняться одновременно на многопроцессорных системах, а так же тем, что последовательность выполнения разных потоков зависит от планировщика операционной системы). Возвращаемся обратно вstart()
, которая подошла к концу, выходим из неё при этом вызывается деструктор у локальной копииptr
, снова уменьшается счётчик ссылок, теперь он 1, как и предполагалось (остался только сохранённый экземляр в листе). Всё отлично, всё как запланировано. Запущенный поток при этом работает до тех пор, пока умный указатель остаётся валидным. - Спим 5 секунд, тем временем на экран будет печататься ’tick!’ через каждую секунду. Максимум будет выведено 4-5 такие надписи.
- Вызываем метод
stop()
, где очищаем список, тем самым у нашего экземпляра умного указателя счётчик ссылок уменьшается до 0 и память освобождается. Поток ловит тот факт, что умный указатель больше не валиден и завершает свою работу. - Делаем
join()
у потока, но т.к. он уже завершился (если нет, то сделает это крайне скоро) - Программа завершается
Так мы думаем она работает. Компилируем, запускаем и… на экран медленно и печально выводится “tick!”
Где же мы ошиблись? Если посмотреть с отладчиком на момент очистки списка у нас счётчик ссылок равен 2 а не 1, как предполагалось.
Всё становится ясно, когда начинаем изучать работу bind. По дизайну, bind создаёт копии всех
аргументов, которые передаются ему и сохраняет их в возвращаемом функторе. Если какой-то из аргументов
является shared_ptr
то мы получаем увеличение счётчика ссылок, которое мы не предусмотрели в
размышлениях выше. Функциональный объект передаётся как аргумент в конструктор boost::thread
и…
дальше не смотрел, что с ним происходит: просто так он не сохраняется, т.к. в случае если делается
detach()
и разрушении объекта thread
счётчик так же не уменьшается. Но, в общем, это и не
важно.
Что бы boost::bind
не работал с копиями объектов, а со ссылками, можно воспользоваться
boost::ref()
или boost::cref()
, но передавать потоку значение по ссылке, особенно, если это
локальный объект, который уничтожится по выходу из области видимости - чревато. Поэтому, в нашем
случае, самый простой вариант, передавать в поток не сильный, а слабый указатель, weak_ptr
, а
уже внутри функции потока, если нужен сильный указатель, попытаться захватить его и использовать.
На основе вышесказанного, исправленная версия кода выше (привожу только ту часть, что поменялась):
void fooThread(MyStructWPtr weak)
{
// Если нужно, захватываем указатель:
MyStructPtr ptr = weak.lock();
// используем
...
// Освобождаем
ptr.reset();
while (!weak.expired())
{
boost::this_thread::sleep(boost::posix_time::seconds(1));
cout << "tick!" << endl;
}
clog << "fooThread exit" << endl;
}
...
// создание потока
thread = boost::thread(boost::bind(fooThread, MyStructWPtr(ptr)));
...