[C++] спецификация многопоточности - часть интерфейса?

Maurog

под интерфейсом мы будем понимать класс с чисто виртуальными функциями
1) считаете ли вы, что необходимо описать (задокументировать) множество исключений, которое может кинуть каждый метод?
2) считаете ли вы, что необходимо описать как данный интерфейс (точнее, объект, реализующий этот интерфейс) можно использовать в многопоточной среде?
интерфейс - это ограничение и это ограничение мне больше нравится называть "контрактом".
вопросы родились в споре, а истина пока не "родилась"
рассмотрим пример:
имеется интерфейс (персонажи вымышлены)

class ObjectAccessor
{
public:
typedef shared_ptr<ObjectAccessor> Ptr;

public:
virtual ~ObjectAccessor {}
virtual Object Read const = 0;
virtual void Save(const Object& newObject) = 0;
};

имеется некая имплементация StandardObjectAccessor, которая адекватно работает в однопоточной среде
написан следующий декоратор

class ThreadSafeObjectAccessor : public ObjectAccessor
{
explicit ThreadSafeObjectAccessor(ObjectAccessor::Ptr impl)
: Impl(impl)
{}
virtual Object Read const
{
AutoLock lock(Mutex);
return Impl->Read;
}
virtual void Save(const Object& newObject)
{
AutoLock lock(Mutex);
return Impl->Save(newObject);
}
private:
const ObjectAccessor::Ptr Impl;
mutable MutexType Mutex;
};

и предоставлена функция для создания потокобезопасного accessor-a из незащищенного:

ObjectAccessor::Ptr DecorateWithThreadSafe(ObjectAccessor::Ptr accessor);

чем хорош класс ThreadSafeObjectAccessor:
1) ответственность по блокировкам вытащена в отдельный класс и не перемешана с основной логикой чтения и записи объекта (привет SRP)
чем плох:
1) блокировка мьютекса осуществляется на неопределенное время, это считается плохи тоном (кстати, я пока не очень проникся этим утверждением, многие утверждают, что мьютексы надо держать как можно меньше времени)
2) причина этого поста: классы StandardObjectAccessor и ThreadSafeObjectAccessor по-разному реализуют интерфейс и тем самым нарушается LSP
аргументы: набор модулей стыкуется по швам - интерфейсам, тем самым повышается модифицируемость, гибкость. модули могут быть многопоточными, именно поэтому они должны четко представлять как можно использовать объекты с данным интерфейсом. только в этом случае можно будет применить OCP, LSP
контраргументы: контракт описывает поведение в однопоточной среде, не более. модуль должен знать качество (потокобезопасен ли) объекта, с которым он работает
приведу цитату из книги "Архитектура программного обеспечения на практике, 2-е издание"
Недавно мы с одним коллегой спорили насчет того, что конкретно следует называть интерфейсом программного элемента; для обоих было очевидно, что именами программ, к которым существует возможность обращения, и принимаемыми ими параметрами определение интерфейса не исчерпывается. Коллега выразил предположение, что на самом деле речь идет о ряде допущений об элементе, которые можем принять с достаточной степенью уверенности и которые варьируют в зависимости от контекста применения этого элемента. Я согласился и показал ему статью Парнаса (Parnas 1971 в которой тот говорил абсолютно о том же.
как вы понимаете эту цитату? следует ли делать допущения про имплементацию? или здесь о чем-то другом?

Serab

1) блокировка мьютекса осуществляется на неопределенное время, это считается плохи тоном (кстати, я пока не очень проникся этим утверждением, многие утверждают, что мьютексы надо держать как можно меньше времени)
Чем тут проникаться? Если у тебя мутекс долго занят, то какой смысл от многопоточности вообще?
Конечно, надо разжимать как только не нужна синхронизация.
2) причина этого поста: классы StandardObjectAccessor и ThreadSafeObjectAccessor по-разному реализуют интерфейс и тем самым нарушается LSP
Интерфейсы созданы, чтобы их по-разному реализовывать :)
Вообще LSP о другом: он о связи между типом и подтипом, а не между подтипами общего типа.
К тому же он довольно формальный: ограничения на состояние типа не должно усиливаться при применении той же операции к подтипу.

Serab

в общем мое мнение такое: может быть частью, если ты этого захочешь. Тогда один из этих двух классов не будет в строгом смысле предоставлять этот интерфейс.
Но если интерфейс не закрепляет этого, то могут появиться и потокобезопасные реализации, в чем проблема-то? Если это проблема, то ты по сути заявляешь следующее: "вот есть интерфейс, но если его реализация не испытывает проблем в многопоточной среде, то это уже не есть реализация этого интерфейса, это какая-то маргинальная, нехорошая реализация. Даешь только потокоопасные реализации!"

evolet

2) считаете ли вы, что необходимо описать как данный интерфейс (точнее, объект, реализующий этот интерфейс) можно использовать в многопоточной среде?
имхо имееют право на жизнь оба варинта (определение поведение в многопоточности включаются в интерфес или не включается) и в разных проектах может быть удобней договориваться по-разному (как и для разных интефейсов в пределах одного проекта)
В случае включения имхо возможна и предложенная реализация (которая может не подойти из-за вопросов производительности, а может и подойти, зависит от ситуации)

erotic

Я обычно делал интерфейс без разделения многопоточность/нет ее, если:
1. Я думаю, что использоваться интерфейс будет только однопоточно.
2. Я знаю, что он предназначен специально для многопоточности.
А смешанных обычно не было.
После прочтения статьи про использование volatile для спецификации потокобезопасных интерфейсов http://www.ddj.com/cpp/184403766 (за что отдельное спасибо un...lu) я решил, что это удобный способ разделать потокобезопасные реализации от непотокобезопасных (как раз тогда понадобились интерфейсы, которые могут быть использованы и так и так). В самом примитивном случае можно сделать заглушку через мьютекс, когда одна ф-я использует другую, но чаще можно что-то более эффективное придумать.
Т.ч. теперь неспецифицированный интерфейс предназначен только для однопоточного использования, только с volatile - только для многопоточного (конечно, никто не мешает и из одного потока его вызывать если в интерфейсе есть оба метода - тогда вызывай как хочешь :)
Считаю, что это в определенный момент сильно упростили мне жизнь. Не приходится однажды ахнуть, поняв, что реализация не рассчитана на многопоточное использование, а используется.
Исключения в коде не специфицирую (через средства языка но пишу в документации. Не все, а только в данном методе которые напрямую могут произойти.

Maurog

Чем тут проникаться? Если у тебя мутекс долго занят, то какой смысл от многопоточности вообще?
мьютекс обеспечивает mutual exclusive access к ресурсу. именно из этого определения я исхожу, когда рассуждаю о необоснованной фразы "нужно отпускать как можно раньше". прагматичные вопросы перформанса не относятся к концептуальной целостности и непротиворечивости понятия мьютекса и не следует ими оперировать.
Интерфейсы созданы, чтобы их по-разному реализовывать
время бежит вперед и люди стали задумываться, как не наступать на одни и те же грабли и как повысить качество кода, а всякие понятия типа OCP, LSP придумали не от хорошей жизни. поэтому это слишком примитивный подход
Вообще LSP о другом: он о связи между типом и подтипом, а не между подтипами общего типа.
К тому же он довольно формальный: ограничения на состояние типа не должно усиливаться при применении той же операции к подтипу.
формальность его изначально была ограничена абстрактными данными, но затем смогли найти удачную проекцию на практику (в частности, грамотное проектирование систем)
http://en.wikipedia.org/wiki/Behavioral_subtyping
http://www.objectmentor.com/resources/articles/lsp.pdf
цитата из pdf:
In order for the LSP to hold, and with it the Open-Closed principle, all derivatives
must conform to the behavior that clients expect of the base classes that they use.

Serab

мьютекс обеспечивает mutual exclusive access к ресурсу. именно из этого определения я исхожу, когда рассуждаю о необоснованной фразы "нужно отпускать как можно раньше". прагматичные вопросы перформанса не относятся к концептуальной целостности и непротиворечивости понятия мьютекса и не следует ими оперировать.
а сразу это сказать слабо было? Если «прагматичными вопросами перформанса» (кстати, почему некоторые слова ты транслитерируешь, а некоторые — нет?) не задаваться, то можно дальше перестать задаваться и вопросами здравого смысла и как раз начать спорить ни о чем.
Поясни тогда постановку вопроса про мьютексы. Только так, чтобы потом не пришлось добавлять новые правила, как ты только что сделал.
Даже в твоей постановке: ну за хрень, у меня данные оказываются надолго недоступными? Это по-твоему нормально? Проникайся.

Serab

время бежит вперед и люди стали задумываться, как не наступать на одни и те же грабли и как повысить качество кода, а всякие понятия типа OCP, LSP придумали не от хорошей жизни. поэтому это слишком примитивный подход
какой подход? Да, примитивно говорить как ты: «интерфейсы реализованы по-разному, это плохо». Сам подумай. Зачем тогда вообще две реализации? Абсурд.

Serab

формальность его изначально была ограничена абстрактными данными, но затем смогли найти удачную проекцию на практику (в частности, грамотное проектирование систем)
http://en.wikipedia.org/wiki/Behavioral_subtyping
http://www.objectmentor.com/resources/articles/lsp.pdf
цитата из pdf:
In order for the LSP to hold, and with it the Open-Closed principle, all derivatives
must conform to the behavior that clients expect of the base classes that they use.
Это я все читал. Только вот ты ожидаешь от этих принципов какой-то магической силы, которая вмиг решит все твои проблемы. А решать-то самому надо. Так вот: почему ты решил, что клиенты не «ожидают» потокобезопасного поведения какой-то реализации интерфейса?

Maurog

спасибо за развернутый ответ
статью почитал, в целом положительное впечатление, хотя в плане подхода к синхронизация есть пробелы у Александреску (можно почитать баталии на рсдн о мемори барьерах, сиквенс пойнтах, балансе между мьютесами и волатайлом и как все это работает на SMP)
использует захват мьютекса на неопределенное время (возможно, в угоду более светлой цели) и не стесняется говорить, что решая проблему с многопоточным доступом к ресурсам, приходится встречаться с дедлоками (оно и неудивительно, ибо буздумная блокировка мьютексов, порожденная введением "удобного класса", будет приводить к дедлокам)
использование квалификатора volatile в интерфейсах для подчеркивания особенности контракта мне кажется интересной идеей
Не приходится однажды ахнуть, поняв, что реализация не рассчитана на многопоточное использование, а используется.
собственно, эти моменты хотелось бы исключить путем создания грамотных систем и мои вопросы следует рассматривать именно с этой колокольни.
возможные исключения мы тоже документируем без использования средств языка

Maurog

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

rosali

статья очень клевая, аж захотелось снова на С++ пописать =)

Serab

В COM (который тут никто не любит и все считают устаревшим например, потокобезопасность специфицируется на уровне компонента, а не интерфейса.
Это один подход. Можно создать вообще два разных интерфейса (ну как в .NET любят делать константные интерфейсы). Это другой. Где проблема?
Еще раз: вот мы с сказали, что да, можно требовать потокобезопасности, а можно не требовать. Что тебе тут не нравится? Имхо, вопрос исчерпан, никто не заставит меня требовать от интерфейса потокоопасности.
Оставить комментарий
Имя или ник:
Комментарий: