Наверное последнее время я достаточно мало пишу кода. Я чуствую, что мне не хватает набитой руки. И вокруг меня как-то мало крутого и правильного кода. И мне кажется, что посади меня сейчас плотно программировать - я бы делал это медленно. Слишком много бы думал. Хотя руководящая нагрузка снялась, кто ж знает что было бы?
Но сейчас про код...
Есть неторопливая фоновая задача, и я ее думаю. Задача заключается примерно в следующем: Есть модуль, отвечающий за взаимодействие между узлами, он сообщения через TLS гоняет. Но есть определенные неудобства в том плане, что пока соединение не установится - мы не знаем куда слать сообщение. Их нужно хранить в ожидании подключений. Сейчас там все достаточно криво и очередь существует толко у уже подключенных соединений.
Нужно сделать буфер, который будет независим от соединений. Соединения сами по себе, а буффер единый и живет своей жизнью. И этот буфер может хранить сообщения для отправки и дефрагментировать сообщения, которые валятся к нам.
Это было краткое вступление.
Сущесвующие классы жестко провязаны через коллбеки, и это не очень удобно. В случае с сетевым буффером вполне можно сделать чистый класс, который почти ни от кого не зависит и все действия осуществляются через его интерфейс. То есть добавляем сообщения на отправку, извлекаем получившиеся фрагменты и отправляем. Аналогично поступаем на вход. Загружаем туда фрагменты - а оттуда достаем собранные сообщения и пробрасываем в систему. Поскольку вся система построена на асинхронщине - такой интерфейс вполне удобен. Вопрос в деталях.
Отправляющая часть.
Сам по себе буфер достаточно умный - умеет следить за своим размером, чистит память если надо. Никто не обещал, что сообщения будут жить вечно. Поэтому в плане добавления сообщений проблем никаких нет.
buffer.addOutgoingMessage(m);Вопрос встает в плане извлечения. Поскольку этим занимается другой поток, он точно не знает - есть сообщения или нет. Но поскольку речь идет о потоках - следующий вариант серьезно не рассматривается. Считаю что каждый метод должен быть самодостаточным. Но мы к нему еще вернемся.
if (buffer.hasOutgoingFragment()) { const auto f = buffer.getOutgoingFragment(); }Если посмотреть на tbb::concurrent_queue, нам предлагается такой вариант:
bool try_pop( T& destination );Такой подход помоему нарушает сразу два принципа.
Fragment f; if (buffer.getOutgoingFragment(f)) { ... }Во первых он нарушает RAII. Это приводит к тому, что сперва срабатывает конструктор по умолчанию, а потом произойдет копирование объекта. Я не сторонник преждевременной оптимизации, но это не значит, что я должен конструировать каждый объект дважды.
Во вторых этот код нарушает принцип Command/Query Separation. Один метод пытается что-то нам вернуть, и одновременно пытается донести до нас - смог он это сделать или не смог. Меня немного коробит от осознания того факта, что f может остаться неинициализированным.
Есть другой способ, не намного лучший, вернуть tuple.
const auto fpair = buffer.getOutgoingFragment(); if (get<0>(fpair)) { // Можно работать с get<1>(pair) }Или даже так:
const auto fpair = buffer.getOutgoingFragment(); if (getИли можно даже имея переменные забиндить их через tie, но в этом случае мы возвращаемся к проблеме RAII.(fpair)) { // Можно работать с get (fpair) }
bool have; Fragment f; tie(have, f) = buffer.getOutgoingFragment(); if (have) { // Можно работать с f }И как-то это все многословно. Хочется чище... Да какого черта, вот чистый и безопасный вариант:
try { const auto f = getOutgoingFragment(); ... } catch (const Buffer::Empty &) { ... }Это похоже на логику на исключениях. Да это она.
Кто-то скажет про скорость, что исключения дескать медленные. Тут есть один ньюанс. Когда сообщений много и идет нагрузка - скорость крайне важна. Но когда сообщений нет, нам пофиг производительность.
Я провел небольшое сравнение, и в случае возвращения значений не заметил разницы между вариантом с исключениями и всеми остальными. Кажется что вариант с референсом немного проигрывает. Но это все на уровне погрешности измерений.
Другое дело, когда сообщений нет и последний вариант бросает исключения. В этом случае он работает в 1000 раз дольше, чем остальные. Но не все ли равно?
Это не все равно, в плане приемной части. Потому что фрагментов может быть на порядки больше, чем собранных сообщений. В этом случае можно совместить два подхода. Метод проверки наличия и метод извлечения с исключением. Это будет безопасно в плане многопоточности.
В принципе для единообразия можно и отправку тоже снабдить методом проверки. Он может быть полезен...
class Buffer { public: void addOutgoingMessage(const Message &m); bool hasOutgoingFragment() const; Fragment getOutgoingFragment(); void addIncomingFragment(const Fragment &f); bool hasIncomingMessage() const; Message getIncomingMessage(); };Здесь нужно понимать, что один сценарий достаточно оптимален, в то время как другой крайне неоптимален. По какому сценарию используется класс? Насколько оптимально его использование?
Логика на исключениях... наверное я буду гореть за это в аду...
0 коммент.:
Отправить комментарий