Статьи

Ловушка умных указателей и неявный обмен данными

Обмен данными между потоками — сложная задача. Любой, кто имеет опыт работы с многопоточным кодом, даст вам 1001 синоним «хитрый», большинство из которых вы, вероятно, не использовали бы перед своими родителями. Однако проблема, о которой я сейчас говорю, не имеет ничего общего с многопоточностью и всем с обменом данными и утечкой абстракции.

Это шаблон, который используется очень часто, когда один объект используется симметрично в начале и в конце жизни другого. То есть предположим, что у нас есть класс, который должен получать уведомления при создании определенного другого класса, а затем снова при его уничтожении. Один из способов добиться этого — просто установить флаг один раз в true и второй раз в false, соответственно, в конструкторе и деструкторе второго объекта.

Этот конкретный пример на C ++, но это просто для иллюстрации шаблона.

class Object
{
public:

Object(SomeComponent& comp) : m_component(comp)
{
    m_component.setOnline(true); // We’re online.
}

~Object()
{
    m_component.setOnline(false); // Offline.
}
};

 
Это выглядит безошибочно, так как нет никакого способа, которым флаг не будет установлен, пока Объект создан и уничтожен как предназначено. Как правило, наш код будет использоваться следующим образом:

Object* pObject = new Object(component);
// component knows we are online and processing...

delete pObject; // Go offline and cleanup.

 
Теперь давайте посмотрим, как кто-то может использовать этот класс …

// use smart pointer to avoid memory leaks...
std::auto_ptr<object> apObject;

// Recreate a new object...
apObject.reset(new Object(component));

Видите проблему? Код с треском проваливается! И это даже не очевидно. Зачем? Потому что существуют неявные предположения и неплотная абстракция на работе Давайте нарежем последнюю строчку …

Object* temp_object = new Object(component); // create new Object
  Object::Object();
    component.setOnline(true);  // was already true!
delete apObject.ptr; // new instance passed to auto_ptr
  Object::~Object(); // old instance deleted
    component.setOnline(false); // OUCH!
apObject.ptr = temp_object;

Видишь что случилось?

Оба автора написали довольно простой код. Они не могли бы добиться большего успеха, не делая предположений, выходящих за рамки их работы. Это паттерн, с которым очень легко столкнуться, и он далек от веселья. Подумайте, как можно было обнаружить проблему в первую очередь. Это не очевидно. Флаг был установлен правильно, но иногда не получится! То есть всякий раз, когда есть экземпляр Object, и мы создаем другой, чтобы заменить первый, флаг в конечном итоге становится false. В первый раз, когда мы создаем объект, все работает нормально. Во второй раз компонент, кажется, не знает о том, что мы установили флаг в true.

Кто-то заметил сбой, предположил, что флаг установлен не всегда или может быть установлен неправильно, просмотрел код класса и убедился, что все было правильно. Глядя на сценарий использования Object, мы не обязательно разбираемся с внутренностями auto_ptr. В конце концов, это строительный блок; шаблон; абстракция блока памяти. Можно было бы быстро взглянуть, увидеть, что экземпляр Object создается и сохраняется в auto_ptr. Опять же ничего необычного.

Итак, почему код потерпел неудачу?

Ответ на нескольких уровнях. Прежде всего, у нас были общие данные, которые не были подсчитаны. Это главный недостаток. Совместно используемые данные являются ответственностью, потому что они не в абстракции экземпляров объектов. Те же самые предположения абстракции, которые делает auto_ptr; это указывает на независимые блоки памяти. Мы бросили вызов предположениям, которые делает auto_ptr, и не смогли защитить наши неявно передаваемые данные.

Другими словами, у нас было два экземпляра Object одновременно, но флаг, который мы обновляли, имел только два состояния: true и false. Таким образом, у него не было никакого способа отследить что-либо кроме единственного фрагмента информации. В нашем случае мы отслеживали, были ли мы онлайн или нет. Автор Объекта сделал очень опасные предположения. Прежде всего, предположение о том, что состояние флага эквивалентно времени жизни объекта, оказалось очень обманчивым. Потому что это подняло вопрос о том, может ли существовать более одного экземпляра объекта. Этот вопрос помог бы избежать многих проблем в будущем, однако он не был очевиден и, возможно, никогда никому не приходил в голову.

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

Если что-то не должно случиться, предотвратите это, сделав невозможным случиться.

Решения

Решения не так просты. Очевидное решение — вынуть вызовы установки флага из Object и вызвать их вручную. Тем не менее, это противоречит тому, что они могут быть забыты или пропущены при вызове, в случае ошибки. Рассмотрим случай, когда мы должны установить флаг в false, когда Object уничтожен, но это происходит из-за исключения, которое автоматически уничтожает экземпляр Object. В таком случае мы должны перехватить исключение и установить указанный флаг в false. Это, конечно, никогда не так просто, как хотелось бы, особенно в сложном и зрелом производственном коде. Действительно, использование автоматических гарантий языка (в данном случае автоматический вызов ctor и dtor) — это явные преимущества, которые мы не можем игнорировать.

Одним из возможных решений является предотвращение создания объекта более одного раза за раз. Но это может быть очень проблематично. Рассмотрим случай, когда у нас есть несколько экземпляров компонентов, и мы заинтересованы в разных объектах для каждого компонента, а не в глобальном уникальном экземпляре объекта.

Как я уже сказал, не простое решение. Решение, которое я бы использовал, является следующим лучшим средством предотвращения создания экземпляров. А именно для подсчета количества экземпляров. Однако, даже если мы ссылаемся на количество объектов или даже вызовы для установки флага во всех случаях, мы должны переопределить контракт. Что значит иметь несколько экземпляров Object и несколько вызовов, чтобы установить флаг в true? Означает ли это, что у нас все еще есть один ответственный объект и что это гарантирует? Что если есть другие ограничения, может ли какой-то другой код принимать только один экземпляр Object, когда этот флаг установлен?

Все вопросы, вытекающие из предлагаемых нами решений, требуют от нас определения или переопределения контрактов и допущений наших объектов. И какое бы решение мы ни согласовали, оно будет иметь свои собственные требования и, возможно, даже допущения, если мы не будем слишком осторожны.

Вывод

Без сомнения, настоятельно рекомендуется использовать шаблоны проектирования и лучшие практики. И все же по иронии судьбы иногда они могут привести к самым неожиданным результатам. Это не критика использования таких рекомендаций от опытных специалистов и лидеров отрасли, скорее, это результат объединения абстракций таким образом, что не только скрывает некоторые очень фундаментальные предположения в нашем проекте и / или реализации, но даже создает ситуации, когда некоторые из неявные предположения нашего кода оспариваются. Представленный случай является хорошим примером. Если бы разработчики не использовали шаблон ctor / dtor для установки указанного флага, или если бы они не использовали auto_ptr, такой проблемы не возникло бы. Хотя у них были бы другие точки отказа, как уже упоминалось.

По общему признанию, без опыта почти невозможно поймать подобные случаи просто, читая код или, предпочтительно, проектируя. И у неопытности нет легкого средства. Но если кто-то поймет хитрость, не стесняйтесь связаться со мной.