Pull to refresh

Динамические структуры в shared-памяти

Reading time5 min
Views5.6K

Приветствую, читатель! Хотелось бы осветить свою небольшую библиотеку для C++, которая призвана помочь Вам создавать динамические структуры в shared-памяти. Далее - под катом.

Зачем это?

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

Первый кейс

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

Каким образом бы можно было это организовать?

Можно, например, сохранять данные в файле. Но как тогда обеспечить синхронизацию чтения и записи? Скорее всего, с этим возникнут сложности.
Есть более удобный и практичный способ - использовать shared-память. В такой памяти можно сохранить и mutex'ы, которые можно инициализировать как shared и синхронизовать операции чтения через них.
Но как тогда удобно описать структуру стека?

Можно, например, вот так:

struct shared_stack {
	int top;
	int& top(int x) {
		return *(int*)((std::size_t)this + sizeof(shared_stack) + top * sizeof(int);
	}
	void push(int x) { /**/ }
	void pop() { /**/ }
};

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

struct shared_stack {
	int top;
	int data[];
	int top() {
		return data[top];
	}
	/**/
};

Да, ISO C++ говорит что так делать нехорошо, зато выглядит сносно.

Хорошо, теперь мы умеем размещать стек в shared-памяти, но что, если наша структура имеет куда более сложную модель?

Второй кейс

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

Получается, используя подход, похожий на подход из первого кейса мы получаем что-то уже очень страшное:

struct shared_stack {
	int max_size;
	int top;
	int data[];
	int top() {
		return data[top];
	}
	/**/
};

struct two_stacks {
	shared_stack first;
	shared_stack& second() {
		return *(shared_stack*)
      ((std::size_t)this + sizeof(shared_stack) + first.max_size * sizeof(int));
	}
};

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

struct two_stacks {
	std::size_t second_offset;
	void init(std::size_t first_size, std::size_t second_size) {
		first().init(first_size);
		// теперь стек умеет вычислять сколько памяти он использует
		second_offset = first().get_size();
		second().init(second_size);
	}

	template<typename T>
	T& get_at_offset(std::size_t offset) {
		return *(T*)((std::size_t)this + sizeof(two_stacks) + offset);
	}

	shared_stack& first() {
		return get_at_offset<shared_stack>(0);
	}

	shared_stack& second() {
		return get_at_offset<shared_stack>(second_offset);
	}
};

Но есть и вторая проблема, которую я не упомянул. Догадались?

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

А если мы захотим реализовать какие-то вложенные структуры, мы совсем закопаемся в вычислениях.

Что я предлагаю?

UPD: все действия с shared-памятью вам необходимо производить самим. Вам понадобится аллокатор, похожий на тот, что использован в коде ниже, только который по-настоящему выделяет shared-память/mmap'ит файлы в память.
При помощи моей небольшой библиотеки можно удобно создавать структуры любой сложности. К примеру, первый кейс выглядел бы вот так:

// Наследуемся от can_be shared,
// чтобы показать, что наша структура может быть
// размещена в shared-памяти
struct shared_stack : can_be_shared {
	// Указатели на наши данные
	int* size; // размер стека
	int* top_; // вершина стека
	int* data; // массив данных
	
	// Конструктор: max_size - максимальный размер стека
	// этот конструктор используется чтобы создавать новые
	// стеки, чтобы использовать существующий надо написать
	// второй конструктор - он ниже
	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) 
			}) {}

	// Инициализует массив data:
	// what - что проинициализовать, idx - индекс элемента
	static void init_data(int& what, std::size_t idx) {
		what = 0;
	}

	// Конструктор по стеку, который уже лежит по адресу ptr
	shared_stack(void* ptr) : 
		// Указываем, что создаём объект из уже созданного
		from_existing(ptr),
		// и передаём поля для подстановки указателей
		can_be_shared({
			existing_field(&size),
			existing_field(&top_),
			existing_array(&data)
			}) {}
	// Стандартная реализация стека
	int& top() {
		return data[*top_ - 1];
	}
	
	void pop() {
		(*top_)--;
	}

	void push(int x) {
		data[(*top_)++] = x;
	}

	bool empty() {
		return !(*top_);
	}
};

// Аллокатор "shared"-памяти (тут используем свой)
std::vector<void*> to_be_free;
void* shared_allocator(std::size_t size) {
	assert(size != 0);
	void* res = malloc(size);
	to_be_free.push_back(res);
	std::cout << "Allocated " << size << " bytes at " << res << "\n";
	return res;
}

int main() {
	shared_stack stack(5);
  // make_shared вернет адрес выделенной памяти
	void* save_ptr = stack.make_shared(shared_allocator);
	stack.push(10);
	shared_stack ref(save_ptr);
	ref.push(12);
	while (!stack.empty()) {
		std::cout << stack.top() << ":" << ref.top() << "\n";
		stack.pop();
	}
	for (auto& e : to_be_free) {
		free(e);
	}
	return 0;
}

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

struct many_stacks : can_be_shared {
	shared_stack* stacks;
	
	static void init_fn(shared_stack& what,
		std::size_t idx,
		std::vector<size_t>& orig_sizes) {
		// Так как память, лежащая по адресу &what еще 
		// не инициализирована, необходимо вызывать конструктор
		// вот таким вот незатейливым образом
		new(&what) shared_stack(orig_sizes[idx]);
	}

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

	many_stacks(void* ptr) :
		from_existing(ptr),
		can_be_shared({
			existing_array(&stacks)
			}) {};
};

Заметим, что нам не пришлось трогать код написанный ранее - это хороший знак!

https://github.com/MrLolthe1st/shrared_structures - тут можно посмотреть код библиотеки, ну, и конечно же, скачать его для использования в своих проектах. Успехов!

Only registered users can participate in poll. Log in, please.
Сталкивались ли вы с подобными кейсами?
13.89% Да, но эта библиотека не была бы мне полезна5
63.89% Нет23
16.67% Да, было бы удобнее, если бы была такая библиотека6
5.56% Нет, но я думаю, что библиотека сделала бы жизнь удобнее2
36 users voted. 15 users abstained.
Tags:
Hubs:
Total votes 6: ↑2 and ↓4-2
Comments27

Articles