Как стать автором
Обновить

Комментарии 27

Да, удобно, спасибо. А вот у меня задача разместить в разделяемой памяти key-value хранилище, да еще сделать так что бы это хранилище синхронизировалось между несколькими хостами.

А я вот писал (еще два года назад) NoSQL БД для работы (нужен доступ из нескольких процессов + обновление), решил тут переделать почти весь бэкенд с PHP на CPP - а, значит, и заменить БД (тк та реализация была написана для использования из-под PHP) - решил сделать такую вот удобную (по моему мнению) библиотеку - чтобы и себе удобно было, и кому-то еще пригодилось. Так что если будут какие-то вопросы - можете написать, уже есть опыт в подобных штуках :)

Вот в этом фрагменте:

	many_stacks(std::vector<std::size_t> sizes) :
		from_existing(ptr),
		can_be_shared({
			array(&stacks, sizes.size(), init_fn, sizes)
			}) {};

Откуда берется ptr для передачи в from_existing?

И в чем смысл передачи sizes по значению?

Вот это: (std::size_t)this, если я правильно понимаю, не есть хорошо, т.к. нет гарантий, что значение указателя, преобразованное в беззнаковое целое, уместится в std::size_t. Для таких целей, если не ошибаюсь, std::uintptr_t должен использоваться. Надеюсь, более знающие люди поправят меня, если я не прав.

Общее впечатление от кода вашей библиотеки (и ваших примеров здесь) стремнстранные. Приведение типов в стиле чистого Си вместо reinterpret_cast/static_cast, typedef вместо using-ов (при этом код на C++17), практически полное отсутствие комментариев.

Еще очень сильно смущает принятое вами решение по дизайну вашей библиотеки: вы заставляете программиста перечислять поля типа два раза:

	shared_stack(int max_size) : 
		can_be_shared({ 
			// Поля структуры
			field(&size, max_size), 
			field(&top_, 0), 
			// массив размера max_size, init_data 
			// инициализирует значения массива
			array(&data, max_size, init_data) 
			}) {}
      
  shared_stack(void* ptr) : 
		// Указываем, что создаём объект из уже созданного
		from_existing(ptr),
		// и передаём поля для подстановки указателей
		can_be_shared({
			existing_field(&size),
			existing_field(&top_),
			existing_array(&data)
			}) {}

Такое дублирование не есть хорошо, оно чревато ошибками в будущем, когда на проект приходит Вася Пупкин и правит код Феди Иванова. Вася наверняка забудет перечислить новое поле в каком-то из этих мест.

Может можно сделать так, чтобы разработчик указывал перечень полей всего лишь один раз?

Ну и по тексту статьи: у меня после прочтения сложилось ощущение, что ваша библиотека берет на себя еще и заботу по работе с shared-памятью. Но оказалось, что это не так, вы лишь помогаете "размечать" блоки, которые кто-то в shared-памяти уже каким-то образом создал (как, собственно, и саму shared-память). ИМХО, это следовало бы описать в тексте.

  1. Тут описался, тут не должен быть вызван этот конструктор.

  2. Чтобы можно было делать так: many_stacks x({3, 4, 5}). Можно сделать передачу по ссылке - но тогда уже так удобно нельзя будет написать.

  3. Покурил доки - переделаю на uintptr_t.

  4. Такие касты позволяют мне получить именно то, что я хочу. Именно просто указатель, только другого типа

  5. Васе Пупкину поможет IDE, которая подскажет, что какое-то поле не инициализировано.

  6. Вот тут неправда. В функцию make_shared передаётся аллокатор, который и должен выделять эту shared-память.

Чтобы можно было делать так: many_stacks x({3, 4, 5}). Можно сделать передачу по ссылке - но тогда уже так удобно нельзя будет написать.

По константной ссылке?

Такие касты позволяют мне получить именно то, что я хочу. Именно просто указатель, только другого типа

Как будто с reinterpret_cast вы такой возможности не имеете.

Вообще посыл был в том, что когда в C++ном коде встречается приведение в стиле чистого Си, то это дурно пахнет и от такого кода лучше держаться подальше. Если вы пишите библиотеку только для себя, тогда нет проблем. Если хотите, чтобы пользовался кто-то еще, тогда хорошо бы следовать принятым именно в C++ практикам. ИМХО, конечно.

Васе Пупкину поможет IDE

Вася Пупкин может написать:

int data{};

и его новое поле будет считаться инициализированным. Не говоря уже про то, что Вася Пупкин может не пользоваться IDE или же может проигнорировать подобную подсказку.

Вот тут неправда. В функцию make_shared передаётся аллокатор, который и должен выделять эту shared-память.

И что, в вашей библиотеке есть такой готовый allocator?

Константная ссылка - ок.

По поводу того, что не надо два раза писать конструктор - у меня пока нет идей как сделать это удобнее, чем есть сейчас.

Нет, аллокатора нет - каждый может написать его по-своему, используя какие ему нужно библиотеки и функции (можно создавать System V-сегменты, можно mmap'ить файл в память - и тд и тп) - моя библиотека всего лишь его вызовет. Пример аллокатора был приведен в статье.

Нет, аллокатора нет - каждый может написать его по-своему

Как раз к этому и относился мой комментарий. После прочтения статьи у меня сложилось ощущение, что подобная функциональность уже реализована в вашей библиотеке и программисту не придется париться самостоятельно с memory-mapped-files или еще чем-то. Но оказалось, что этого из коробки нет. ИМХО, если вы дополните статью таким дисклаймером, то тогда отсутствие готовых аллокаторов для shared-memory не будет сюрпризом для тех, кто заинтересуется вашей библиотекой.

поясните еще раз - shared память выделить нужно извне и синхронизировать доступ тоже ?

в чем смысл библиотеки - ускользает от понимания

много-процессорный пример был бы полезен )

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

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

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

Грубо говоря, в продюсере мне нужно что-то вроде:

class package {
  char topic_[];
  char payload_[];
public:
  package(std::string_view topic, std::string_view payload) {
    ... // Какое-то перемещение topic в topic_ и payload в payload_.
  }
};

А в процессе-консумере мне хочется получить что-то вроде:

class package {
  ... // Не очень понимаю что.
public:
  package(void * ptr) {... /* Какая-то магия */ }
  
  std::string_view topic() const { ... }
  std::string_view payload() const { ... }
};

Будет что-то такое:

class package : public can_be_shared {
	char* topic_;
	char* payload_;
public:
	inline static void init_view(char& c, std::size_t idx, const char* place) {
		c = place[idx];
	}

	package(std::string_view topic, std::string_view payload) :
		can_be_shared({
			array(&topic_, topic.size() + 1, init_view, topic.data()),
			array(&payload_, payload.size() + 1, init_view, payload.data())
			}) {}
  
	package(void* ptr) : from_existing(ptr),
		can_be_shared({ existing_array(&topic_), existing_array(&payload_) }) {}

	std::string_view topic() const { return topic_; }
	std::string_view payload() const { return payload_; }

};
	std::string_view topic() const { return topic_; }
	std::string_view payload() const { return payload_; }

Вопрос №1: откуда здесь возьмется размер для topic и payload. Вы предполагаете, что размер будет высчитываться поиском 0-символа, как в обычных Си-ных строках?

Вопрос №2: откуда возьмутся значения указателей topic_ и payload_ в процессе-консумере? Да и, честно говоря, не очень понятно на что они будут указывать в процессе-продюсере.

Впечатление по первому примеру — автору нужно, чтобы объекты корректно работали на shared-памяти, то есть оба процесса могли одновременно класть значения в стек и извлекать…

Однако дальше я потерял нить рассуждений. Понятно, что классы, которые пишет автор в дальшейших примерах, уже не могут работать в совместном режиме, т.к. в разных процессах shared-память маппится на разные адреса, а потому у классов не может быть полей-указателей (в том числе в объектах членах, как например есть std::vector pointers_to_init в can_be_shared).

Далее, посмотрев на make_shared, у меня сложилось впечатление, что автор пытается скопировать объект в другой адрес, подправив указатели, чтобы он корректно прочитался в другом месте (позабыв при этом про указатели внутри std::vector?)

Поскольку первоначальную цель (совместная работа на живую) очевидно не достигаем, перенос объекта можно сделать намного проще и понятнее — сериализацией. Тот же protobuf, или куча других библиотек. Сериализованные данные можно копировать через shared memory, без всех этих сложностей, в которых стороннему человеку очень сложно разобраться.

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

Цель вовсе в другом. Копировать данные через shared-память можно и при помощи очередей (из того же System V) и как их сериализовать - уже другая проблема. Там в тегах указано NoSQL. Например, эта библиотека лично мне нужна для того, чтобы переписать уже существующую NoSQL БД (которая должна работать достаточно быстро -> даже скорости просто mmap'нутого файла не достаточно) более простым способом. Там надо хранить колонки, индексы, значения. И это куда более трудоёмкая задача нежели сохранение объектов на стеке.

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

Почему Вы вдруг решили, что я хочу передавать объекты другому процессу?

Потому что в заголовке статьи есть "shared-память".

Передавать объекты другому процессу можно через System V очереди, зачем для этого использовать shared-память? Она используется для хранения структур данных в памяти.

зачем для этого использовать shared-память?

Мало ли. Может нужно передавать блоки данных размером в сотни мегабайт.

Она используется для хранения структур данных в памяти.

У меня есть ощущение, что у вас какое-то собственное понятие "shared-память". Вроде бы когда говорят про shared memory, то имеют в виду именно общую для нескольких процессов память.

Так я всё равно не понял, почему нельзя использовать System V очереди?

Как на счет вот этого: "нужно передавать блоки данных размером в сотни мегабайт." Очереди System V же копируют данные из адресного пространства одного процесса в адресное пространство другого. Копирование сотни мегабайт так себе удовольствие.

Кроме того, одним Linux-ом мир не ограничивается, есть еще, как минимум Windows.

Кроме того, могут быть сценарии, когда этот разделяемый блок на сотню мегабайт должен быть одновременно доступен нескольким процессам на запись. Т.е. сперва один процесс поменял что-то в каком-то месте этого блока и изменения стали видны другим процессам, потом еще какой-то процесс поменял что-то в другой части блока.

Теперь, пожалуйста, удовлетворите мое любопытство: что вы сами понимаете под термином "shared-память"?

Если цель — файловая БД, и объектики 'can_be_shared' кладутся в отмапленный на память файл, то вот это нужно бы переписать:
Тот вектор, который лежит в can_be_shared нужен чтобы мы могли управлять тем, куда указывают указатели полей
Это метаданные, относящиеся к классу, нужно их вынести наружу во вспомогательный класс. Копировать их во все экземпляры объектов класса, тратить время на инициализацию и деаллокацию, слишком большой оверхед.

Особенно, когда рядом заявляется
даже скорости просто mmap'нутого файла не достаточно

Нет, не файловая, я же писал, что используется System V shared-сегменты. Во-первых, достаточно одной инициализации во время запуска. (Те создать объекты всех нужных мне таблиц можно перед началом выполнения кода). Во-вторых, копировать объекты запрещено (зачем их копировать, зачем вам вообще в рамках одного процесса несколько объектов, указывающих на одну таблицу, если можно передавать по ссылке уже существующие?) Так что потратить один раз несколько десятков миллисекунд на инициализацию объекта не так страшно, ведь потом не надо делать никаких вычислений над указателями.
Так что не вижу тут никакого оверхеда. https://pastebin.com/ZjyRxRsJ

Я то думал, что объекты-наследники `can_be_shared` — объекты бизнес-логики, и их в приложении может быть миллионы. И в каждом — вектор с метаданными, и его инициализация.

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

А boost::interprocess не тоже самое делает? Там и аллокаторы уже реализованы

boost явно тяжелее, чем файл в 300 строк.

boost явно более отлажен, чем эти 300 строк

кстати, какая цель была для написанию - просто интересно или альтернативы не устроили ?

Если это не первоапрельское, то годный сборник undefined behavior в стиле си с множеством примеров как НЕ надо делать

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории