Инструменты пользователя

Инструменты сайта


// Boost shared_ptr, bind и thread

Правило: при использовании 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;
}

Практическая польза данного листинга нулевая. Но если посмотреть на него вниматерльно и подумать:

  1. Сначала мы создаём объект типа Foo затем вызываем метод start() в котором создаём умный указатель ptr (точка (0)), здесь у нас счётчик ссылок равен 1. Затем мы добавляем его в список ptrList (точка (1)), здесь счётчик ссылок у нас увеличивается на 1 и становится равным 2, понятно почему так происходит - в списке сохраняется копия объета (используется копирующий конструктор shared_ptr). В точке (2) создаётся нить, которой, в качестве параметра передаётся (при помощи bind) наш умный указатель, логично думаем, раз в функцию потока передаём его по значению, то ещё один раз задействуется копирующий конструктор shared_ptr и счётчик ссылок становится равным 3 в точке (4). В точке (5) говорим, что мы прекратили работать с этой копией указателя, тем самым счётчик ссылок уменьшился на 1 и стал равен 2 (пренебрегаем тем, что код может выполняться одновременно на многопроцессорных системах, а так же тем, что последовательность выполнения разных потоков зависит от планировщика операционной системы). Возвращаемся обратно в start(), которая подошла к концу, выходим из неё при этом вызывается деструктор у локальной копии ptr, снова уменьшается счётчик ссылок, теперь он 1, как и предполагалось (остался только сохранённый экземляр в листе). Всё отлично, всё как запланировано. Запущенный поток при этом работает до тех пор, пока умный указатель остаётся валидным.
  2. Спим 5 секунд, тем временем на экран будет печататься 'tick!' через каждую секунду. Максимум будет выведено 4-5 такие надписи.
  3. Вызываем метод stop(), где очищаем список, тем самым у нашего экземпляра умного указателя счётчик ссылок уменьшается до 0 и память освобождается. Поток ловит тот факт, что умный указатель больше не валиден и завершает свою работу.
  4. Делаем join() у потока, но т.к. он уже завершился (если нет, то сделает это крайне скоро)
  5. Программа завершается

Так мы думаем она работает. Компилируем, запускаем и… на экран медленно и печально выводится «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)));
...

Комментарии