Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Пять подводных камней при применении shared_ptr

Anna | 24.06.2014 | нет комментариев

Класс shared_ptr — это комфортный инструмент, тот, что может решить уйма задач разработчика. Впрочем для того, Дабы не делать ошибок, нужно отменно знать его устройство. Верю, моя статья будет пригодна тем, кто только начинает трудиться с этим инструментом.

Я расскажу о дальнейшем:

  • что такое перекрестные ссылки;
  • чем опасны безымянные shared_ptr;
  • какие угрозы подстерегают при применении shared_ptr в многопоточной среде;
  • о чем значимо помнить, создавая свою собственную освобождающую функцию для shared_ptr;
  • какие существуют особенности применения образца enable_shared_from_this.


Описанные задачи имеют место как для boost::shared_ptr, так и для std::shared_ptr. В конце статьи вы обнаружите приложение с полными текстами программ, написанных для демонстрации описываемых особенностей (на примере библиотеки boost).

Перекрестные ссылки

Данная задача является особенно знаменитой и связана с тем, что указатель shared_ptr основан на подсчете ссылок. Для экземпляра объекта, которым обладает shared_ptr, создается счетчик. Данный счетчик является всеобщим для всех shared_ptr, указывающих на данный объект.

При конструировании нового объекта создается объект со счетчиком и в него помещается значение 1. При копировании счетчик возрастает на 1. При вызове деструктора (либо при замене указателя путем присваивания, либо вызова reset), счетчик уменьшается на 1.

Разглядим пример:

struct Widget {
    shared_ptr<Widget> otherWidget;
};

void foo() {
    shared_ptr<Widget> a(new Widget);
    shared_ptr<Widget> b(new Widget);
    a->otherWidget = b;
    // В этой точке у второго объекта счетчик ссылок = 2
    b->otherWidget = a;
    // В этой точке у обоих объектов счетчик ссылок = 2
}

Что произойдет при выходе объектов a и b из области определения? В деструкторе уменьшатся ссылки на объекты. У всякого объекта будет счетчик = 1 (чай a все еще указывает на b, а b — на a). Объекты “держат” друг друга и у нашего приложения нет вероятности получить к ним доступ — эти объекты “потеряны”.
Для решения этой задачи существует weak_ptr. Одним из классических случаев создания перекрестных ссылок является случай, когда один объект обладает коллекцией других объектов

struct RootWidget {
    list<shared_ptr<class Widget> > widgets;
};

struct Widget {
    shared_ptr<class RootWidget> parent;
};

При таком устройстве всякий Widget будет препятствовать удалению RootWidget и напротив.

В таком случае необходимо ответить на вопрос: “Кто кем обладает?”. Видимо, что именно RootWidget в данном случае обладает объектами Widget, а не напротив. Следственно модифицировать пример необходимо так:

struct Widget {
    weak_ptr<class RootWidget> parent;
};

Слабые ссылки не препятствуют удалению объекта. Они могут быть преобразованы в мощные двумя методами:

1) Конструктор shared_ptr

weak_ptr<Widget> w = …;
// В случае, если объект теснее удален, в конструкторе shared_ptr будет сгенерировано исключение
shared_ptr<Widget> p( w );

2) Способ lock

weak_ptr<Widget> w = …;
// В случае, если объект теснее удален, то p будет пустым указателем

if( shared_ptr<Widget> p = w.lock() ) {
// Объект не был удален – с ним дозволено трудиться
}

Итог:
В случае происхождения в коде кольцевых ссылок, используйте weak_ptr для решения задач.

Безымянные указатели

Задача безымянных указателей относится в вопросу о “точках следования” (sequence points)

// shared_ptr, тот, что передается в функцию foo - безымянный
foo( shared_ptr<Widget>(new Widget), bar() );

// shared_ptr, тот, что передается в функцию foo имеет имя p
shared_ptr<Widget> p(new Widget);
foo( p, bar() );

Из этих 2-х вариантов документация рекомендует неизменно применять 2-й — давать указателям имена. Разглядим пример, когда функция bar определена вот так:

int bar() {
    throw std::runtime_error(“Exception from bar()”);
}

Дело в том, что в первом случае порядок конструирования не определен. Все зависит от определенного компилятора и флагов компиляции. Скажем, это может случиться так:

  1. new Widget
  2. вызов функции bar
  3. проектирование shared_ptr
  4. вызов функции foo

Наверно дозволено быть уверенным лишь в том, что вызов foo будет последним действием, а shared_ptr будет сконструирован позже создания объекта (new Widget). Впрочем никаких гарантий того, что он будет сконструирован сразу позже создания объекта, нет.

Если во время второго шага будет сгенерировано исключение (а оно в нашем примере сгенерировано будет), то Widget будет считаться сконструированным, но shared_ptr еще не будет им обладать. В результате ссылка на данный объект будет утрачена. Я проверил данный пример на gcc 4.7.2. Порядок вызова был таким, что shared_ptr new вне зависимости от опций компиляции не разделялись вызовом bar. Но полагаться именно на такое поведение не стоит – это не гарантировано. Буду благодарен, если мне подскажут компилятор, его версию и опции компиляции, для которых сходственный код приведет к ошибке.

Итог:
Давайте shared_ptr имена, даже если код будет от этого менее лаконичным.

Задача применения в различных потоках

Подсчет ссылок в shared_ptr построен с применением атомарного счетчика. Мы без опаски используем указатели на один и тот же объект из различных потоков. Во каждом случае, мы не привыкли волноваться о подсчете ссылок (потокобезопасность самого объекта – иная задача).

Возможен, у нас есть всеобщий shared_ptr:

shared_ptr<Widget> globalSharedPtr(new Widget);

void read() {
    shared_ptr<Widget> x = globalSharedPtr;
    // Сделать что-нибудь с Widget
}

Запустите вызов read из различных потоков и вы увидите, что никаких задач в коде не появляется (до тех пор, пока вы исполняете над Widget потокобезопасные для этого класса операции).

Возможен, есть еще одна функция:

void write() {
     globalSharedPtr.reset( new Widget );
}

Устройство shared_ptr довольно трудно, следственно я приведу код, тот, что схематически поможет показать загвоздку. Разумеется, подлинный код выглядит напротив.

shared_ptr::shared_ptr(const shared_ptr<T>& x) {
A1:    pointer = x.pointer;
A2:    counter = x.counter;
A3:    atomic_increment( *counter );
}

shared_ptr<T>::reset(T* newObject) {
B1:    if( atomic_decrement( *counter ) == 0 ) {
B2:        delete pointer;
B3:        delete counter;
B4:    }
B5:    pointer = newObject;
B6:    counter = new Counter;
}

Возможен, 1-й поток начал копировать globalSharedPtr (read), а 2-й поток вызывает reset для этого же экземпляра указателя (write). В выводе может получиться следующее:

  1. Поток1 только что исполнил строку A2, но еще не перешел к строке A3 (атомарный инкремент).
  2. Поток2 в это время уменьшил счетчик на строке B1, увидел, что позже уменьшения счетчик стал равен нулю и исполнил строки B2 и B3.
  3. Поток1 доходит до строки A3 и пытается атомарно увеличить счетчик, которого теснее нет.

А может быть и так, что поток1 на строке A2 поспеет увеличить счетчик до того, как поток2 вызовет удаление объектов, но позже того как поток2 произвел уменьшение счетчика. Тогда мы получим новейший shared_ptr, указывающий на удаленный счетчик и объект.

Дозволено написать сходственный код:

shared_ptr<Widget> globalSharedPtr(new Widget);
mutex_t globalSharedPtrMutex;

void resetGlobal(Widget* x) {
    write_lock_t l(globalSharedPtrMutex);
    globalSharedPtr.reset( x );
}

shared_ptr<Widget> getGlobal() {
    read_lock_t l(globalSharedPtrMutex);
    return globalSharedPtr;
}

void read() {
    shared_ptr<Widget> x = getGlobal();
    // Вот с этим x сейчас дозволено трудиться
}

void write() {
     resetGlobal( new Widget );
}

Сейчас, применяя такие функции, дозволено безвредно трудиться с этим shared_ptr.

Итог: если какой-то экземпляр shared_ptr доступен различным потокам и может быть модифицирован, то нужно позаботиться о синхронизации доступа к этому экземпляру shared_ptr.

Особенности времени уничтожения освобождающего функтора для shared_ptr

Данная задача может иметь место только в том случае, если вы используете личный освобождающий функтор в сочетании со слабыми указателями (weak_ptr). Скажем, вы можете сделать shared_ptr на основе иного shared_ptr, добавив новое действие перед удалением (по сути образец “Фасад”). Так вы могли бы получить указатель для работы с базой данных, изъяв его из пула соединений, а по окончании работы заказчика с указателем — воротить его обратно в пул.

typedef shared_ptr<Connection> ptr_t;

class ConnectionReleaser {
    list<ptr_t>& whereToReturn;
    ptr_t connectionToRelease;
public:
    ConnectionReleaser(list<ptr_t>& lst, const ptr_t& x):whereToReturn(lst), connectionToRelease(x) {}

    void operator()(Connection*) {
        whereToReturn.push_back( connectionToRelease );
// Обратите внимание на следующую строчку
        connectionToRelease.reset();
    }
};

ptr_t getConnection() {
    ptr_t c( connectionList.back() );
    connectionList.pop_back();
    ptr_t r( c.get(), ConnectionReleaser( connectionList, c ) );
    return r; 
}

Задача заключается в том, что объект, переданный в качестве освобождающего функтора для shared_ptr, будет разломан только тогда, когда все ссылки на объект будут ликвидированы — как мощные(shared_ptr), так и слабые(weak_ptr). Таким образом, если ConnectionReleaser не позаботится о том, Дабы “отпустить” переданный ему указатель (connectionToRelease), он будет удерживать мощную ссылку, пока существует правда бы один weak_ptr от shared_ptr, сделанного функцией getConnection. Это может привести к довольно неприятному и непредвиденному поведению вашего приложения.

Допустим так же вариант, когда вы воспользуетесь bind для создания освобождающего функтора. Скажем так:

void releaseConnection(std::list<ptr_t>& whereToReturn, ptr_t& connectionToRelease) {
    whereToReturn.push_back( connectionToRelease );
    // Обратите внимание на следующую строчку
    connectionToRelease.reset();
}

ptr_t getConnection() {
    ptr_t c( connectionList.back() );
    connectionList.pop_back();
    ptr_t r( c.get(), boost::bind(&releaseConnection, boost::ref(connectionList), c) );
    return r;
}

Помните, что bind копирует переданные ему доводы (помимо случая с применением boost::ref), и если среди них будет shared_ptr, то его тоже следует очистить, чтобы избежать теснее описанной задачи.

Итог: Исполните в освобождающей функции все действия, которые нужно совершить при уничтожении последней мощной ссылки. Сбросьте все shared_ptr, которые по какой-то причине являются членам вашего функтора. Если вы используете bind, то не забывайте, что он копирует переданные ему доводы.

Особенности работы с образцом enable_shared_from_this

Изредка требуется получить shared_ptr из способов самого объекта. Попытка создания нового shared_ptr от this приведет к неопределенному поведению (скорее каждого к аварийному заключению программы), в различие от intrusive_ptr, для которого это является традиционной практикой. Для решения этой задачи был придуман шаблонный класс-примесь enable_shared_from_this.

Образец enable_shared_from_this устроен дальнейшим образом: внутри класса содержится weak_ptr, в тот, что при конструировании shared_ptr помещается ссылка на данный самый shared_ptr. При вызове способа shared_from_this объекта, weak_ptr преобразуется в shared_ptr через конструктор. Схематически образец выглядит так:

template<class T> 
class enable_shared_from_this {
    weak_ptr<T> weak_this_;
public:
    shared_ptr<T> shared_from_this() {
        // Реформирование слабой ссылки в мощную через конструктор shared_ptr
        shared_ptr<T> p( weak_this_ );
        return p;
    }
};

class Widget: public enable_shared_from_this<Widget> {};

Конструктор shared_ptr для этого случая схематически выглядит так:

shared_ptr::shared_ptr(T* object) {
    pointer = object;
    counter = new Counter;
    object->weak_this_ = *this;
}

Значимо понимать, что при конструировании объекта weak_this_ еще ни на что не указывает. Положительная ссылка в нем появится только позже того, как сконструированный объект будет передан в конструктор shared_ptr. Любая попытка вызова shared_from_this из конструктора приведет к bad_weak_ptr исключению.

struct BadWidget: public enable_shared_from_this<BadWidget> {
    BadWidget() {
        // При вызове shared_from_this() будет сгенерировано bad_weak_ptr
         cout << shared_from_this() << endl;
    }
};

К таким же итогам приведет попытка обратиться к shared_from_this из деструктора, но теснее по иной причине: в момент уничтожения объекта теснее считается, что на него не указывает никаких мощных ссылок (счетчик декрементирован).

struct BadWidget: public enable_shared_from_this<BadWidget> {
    ~BadWidget() {
        // При вызове shared_from_this() будет сгенерировано bad_weak_ptr
        cout << shared_from_this() << endl;
    }
};

Со вторым случаем (деструктор) немного что дозволено придумать. Исключительный вариант — позаботиться о том, Дабы не вызывать shared_from_this и сделать так, Дабы этого не делали функции, которые вызывает деструктор.

С первым случаем все обстоит немножко проще. Наверно вы теснее решили, что исключительный метод для существования вашего объекта — shared_ptr, тогда будет целесообразно переместить конструктор объекта в закрытую часть класса и сделать статический способ для создания shared_ptr надобного вам типа. Если при инициализации объекта вам необходимо исполнить действия, которым понадобится shared_from_this, то для этой цели дозволено выделить логику в способ init.

class GoodWidget: public enable_shared_from_this<GoodWidget> {
        void init() {
                cout << shared_from_this() << endl;
        }
public:
        static shared_ptr<GoodWidget> create() {
                shared_ptr<GoodWidget> p(new GoodWidget);
                p->init();
                return p;
        }
};

Итог:
Избегайте вызовов (прямых либо косвенных) shared_from_this из конструкторов и деструкторов. В случае, если для верной инициализации объекта требуется доступ к shared_from_this: сделайте способ init, делегируйте создание объекта статическому способу и сделайте так, Дабы объекты дозволено было создавать только с поддержкой этого способа.

Завершение

В статье рассмотрены 5 особенностей применения shared_ptr и представлены всеобщие рекомендации по избежанию возможных задач.

Правда shared_ptr и снимает с разработчика уйма задач, познание внутреннего устройства (хоть и приблизительно) является непременным для грамотного применения shared_ptr. Я рекомендую наблюдательно исследовать устройство shared_ptr, а так же классов, связанных с ним. Соблюдение ряда примитивных правил может уберечь разработчика от неугодных задач.

Письменность

Приложение

В приложении представлены полные тексты программ для иллюстрации описанных в статье случаев

Демонстрация задачи кольцевых ссылок

#include <string>
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>

class BadWidget {
	std::string name;
	boost::shared_ptr<BadWidget> otherWidget;
public:
	BadWidget(const std::string& n):name(n) {
		std::cout << "BadWidget " << name << std::endl;
	}

	~BadWidget() {
		std::cout << "~BadWidget " << name << std::endl;
	}

	void setOther(const boost::shared_ptr<BadWidget>& x) {
		otherWidget = x;
		std::cout << name << " now points to " << x->name << std::endl;
	}
};

class GoodWidget {
	std::string name;
	boost::weak_ptr<GoodWidget> otherWidget;
public:
	GoodWidget(const std::string& n):name(n) {
		std::cout << "GoodWidget " << name << std::endl;
	}

	~GoodWidget() {
		std::cout << "~GoodWidget " << name << std::endl;
	}

	void setOther(const boost::shared_ptr<GoodWidget>& x) {
		otherWidget = x;
		std::cout << name << " now points to " << x->name << std::endl;
	}
};

int main() {
	{ // В этом примере происходит утрата памяти
		std::cout << "====== Example 3" << std::endl;
		boost::shared_ptr<BadWidget> w1(new BadWidget("3_First"));
		boost::shared_ptr<BadWidget> w2(new BadWidget("3_Second"));
		w1->setOther( w2 );
		w2->setOther( w1 );
	}
	{ // А в этом примере использован weak_ptr и утраты памяти не происходит
		std::cout << "====== Example 3" << std::endl;
		boost::shared_ptr<GoodWidget> w1(new GoodWidget("4_First"));
		boost::shared_ptr<GoodWidget> w2(new GoodWidget("4_Second"));
		w1->setOther( w2 );
		w2->setOther( w1 );
	}
	return 0;
}

Демонстрация реформирования weak_ptr в shared_ptr

#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>

class Widget {};

int main() {
	boost::weak_ptr<Widget> w;
	// В этой точке weak_ptr ни на что не указывает
	// Способ lock вернет пустой указатель
	std::cout << __LINE__ << ": " << w.lock().get() << std::endl;
	// Проектирование shared_ptr от этого указателя приведет к исключению
	try {
		boost::shared_ptr<Widget> tmp ( w );
	} catch (const boost::bad_weak_ptr&) {
		std::cout << __LINE__ << ": bad_weak_ptr" << std::endl;
	}

	boost::shared_ptr<Widget> p(new Widget);
	// Сейчас у weak_ptr есть значение
	w = p;

	// Способ lock вернет верный указатель
	std::cout << __LINE__ << ": " << w.lock().get() << std::endl;
	// Проектирование shared_ptr от этого указателя тоже вернет верный указатель. Исключения не будет
	std::cout << __LINE__ << ": " << boost::shared_ptr<Widget>( w ).get() << std::endl;

	// Сбросим указатель
	p.reset();
	// Крепких ссылок огромнее нет. У weak_ptr истек срок годности

	// Способ lock вновь вернет пустой указатель
	std::cout << __LINE__ << ": " << w.lock().get() << std::endl;
	// Проектирование shared_ptr от этого указателя вновь приведет к исключению
	try {
		boost::shared_ptr<Widget> tmp ( w );
	} catch (const boost::bad_weak_ptr&) {
		std::cout << __LINE__ << ": bad_weak_ptr" << std::endl;
	}
	return 0;
}

Демонстрация задачи многопоточности для shared_ptr

#include <iostream>

#include <boost/thread.hpp>
#include <boost/shared_ptr.hpp>

typedef boost::shared_mutex mutex_t;
typedef boost::unique_lock<mutex_t> read_lock_t;
typedef boost::shared_lock<mutex_t> write_lock_t;

mutex_t globalMutex;
boost::shared_ptr<int> globalPtr(new int(0));

const int readThreads = 10;
const int maxOperations = 10000;

boost::shared_ptr<int> getPtr() {
// Закомментируйте следующую строку, Дабы ваше приложение упало
	read_lock_t l(globalMutex);
	return globalPtr;
}

void resetPtr(const boost::shared_ptr<int>& x) {
// Закомментируйте следующую строку, Дабы ваше приложение упало
	write_lock_t l(globalMutex);
	globalPtr = x;
}

void myRead() {
	for(int i = 0; i < maxOperations;   i) {
    	boost::shared_ptr<int> p = getPtr();
	}
}

void myWrite() {
	for(int i = 0; i < maxOperations;   i) {
		resetPtr( boost::shared_ptr<int>( new int(i)) );
	}
}

int main() {
	boost::thread_group tg;
	tg.create_thread( &myWrite );
	for(int i = 0; i < readThreads;   i) {
		tg.create_thread( &myRead  );
	}
	tg.join_all();
	return 0;
}

Демонстрация задачи deleter weak_ptr

#include <string>
#include <list>
#include <iostream>
#include <stdexcept>

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/bind.hpp>

class Connection {
	std::string name;
public:
	const std::string& getName() const { return name; }

	explicit Connection(const std::string& n):name(n) {
		std::cout << "Connection " << name << std::endl;
	}

	~Connection() {
		std::cout << "~Connection " << name << std::endl;
	}
};

typedef boost::shared_ptr<Connection> ptr_t;

class ConnectionPool {
	std::list<ptr_t> connections;

	// Данный класс предуготовлен для демонстрации первого варианта создания deleter (get1)
	class ConnectionReleaser {
		std::list<ptr_t>& whereToReturn;
		ptr_t connectionToRelease;
	public:
		ConnectionReleaser(std::list<ptr_t>& lst, const ptr_t& x):whereToReturn(lst), connectionToRelease(x) {}

		void operator()(Connection*) {
			whereToReturn.push_back( connectionToRelease );
			std::cout << "get1: Returned connection " << connectionToRelease->getName() << " to the list" << std::endl;

			// Закомментируйте след. строку и обратите внимание на разницу в выходной печати
			connectionToRelease.reset();
		}
	};

	// Эта функция предуготовлена для демонстрации второго варианта создания deleter (get2)
	static void releaseConnection(std::list<ptr_t>& whereToReturn, ptr_t& connectionToRelease) {
		whereToReturn.push_back( connectionToRelease );
		std::cout << "get2: Returned connection " << connectionToRelease->getName() << " to the list" << std::endl;

		// Закомментируйте следующую строку и обратите внимание на разницу в выходной печати
		connectionToRelease.reset();
	}

	ptr_t popConnection() {
		if( connections.empty() ) throw std::runtime_error("No connections left");
		ptr_t w( connections.back() );
		connections.pop_back();
		return w;
	}
public:
	ptr_t get1() {
		ptr_t w = popConnection();
		std::cout << "get1: Taken connection " << w->getName() << " from list" << std::endl;
		ptr_t r( w.get(), ConnectionReleaser( connections, w ) );
		return r;
	}

	ptr_t get2() {
		ptr_t w = popConnection();
		std::cout << "get2: Taken connection " << w->getName() << " from list" << std::endl;
		ptr_t r( w.get(), boost::bind(&releaseConnection, boost::ref(connections), w ));
		return r;
	}

	void add(const std::string& name) {
		connections.push_back( ptr_t(new Connection(name)) );
	}

	ConnectionPool() {
		std::cout << "ConnectionPool" << std::endl;
	}

	~ConnectionPool() {
		std::cout << "~ConnectionPool" << std::endl;
	}
};

int main() {
	boost::weak_ptr<Connection> weak1;
	boost::weak_ptr<Connection> weak2;
	{
		ConnectionPool cp;
		cp.add("One");
		cp.add("Two");

		ptr_t p1 = cp.get1();
		weak1 = p1;
		ptr_t p2 = cp.get2();
		weak2 = p2;
	}
	std::cout << "Here the ConnectionPool is out of scope, but weak_ptrs are not" << std::endl;
	return 0;
}

Демонстрация задачи с enable_shared_from_this

#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>

class BadWidget1: public boost::enable_shared_from_this<BadWidget1> {
public:
	BadWidget1() {
		std::cout << "Constructor" << std::endl;
		std::cout << shared_from_this() << std::endl;
	}
};

class BadWidget2: public boost::enable_shared_from_this<BadWidget2> {
public:
	~BadWidget2() {
		std::cout << "Destructor" << std::endl;
		std::cout << shared_from_this() << std::endl;
	}
};

class GoodWidget: public boost::enable_shared_from_this<GoodWidget> {
	GoodWidget() {}

	void init() {
		std::cout << "init()" << std::endl;
		std::cout << shared_from_this() << std::endl;
	}
public:
	static boost::shared_ptr<GoodWidget> create() {
		boost::shared_ptr<GoodWidget> p(new GoodWidget);
		p->init();
		return p;
	}
};

int main() {
	boost::shared_ptr<GoodWidget> good = GoodWidget::create();
	try {
		boost::shared_ptr<BadWidget1> bad1(new BadWidget1);
	} catch( const boost::bad_weak_ptr&) {
		std::cout << "Caught bad_weak_ptr for BadWidget1" << std::endl;
	}
	try {
		boost::shared_ptr<BadWidget2> bad2(new BadWidget2);
		// При компиляции с новым эталоном вы получите terminate
		// т.к. считается, что из деструктора по умолчанию не может быть сгенерировано исключение
	} catch( const boost::bad_weak_ptr&) {
		std::cout << "Caught bad_weak_ptr for BadWidget2" << std::endl;
	}
	return 0;
}

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB