mirror of
https://github.com/boostorg/website-v2-docs.git
synced 2026-01-27 07:22:16 +00:00
870 lines
27 KiB
Plaintext
870 lines
27 KiB
Plaintext
////
|
|
Copyright (c) 2024 The C++ Alliance, Inc. (https://cppalliance.org)
|
|
|
|
Distributed under the Boost Software License, Version 1.0. (See accompanying
|
|
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
Official repository: https://github.com/boostorg/website-v2-docs
|
|
////
|
|
= High-Performance Database Engine
|
|
:navtitle: Database Engine
|
|
|
|
Creating a high-performance database application in pass:[C++] involves a range of tasks, including efficient data structures, shared and optimized memory management, safe message and network communication, persistent storage, and so much more. This section examines how to get started.
|
|
|
|
[square]
|
|
* <<Libraries>>
|
|
* <<Sample Database Engine using Containers>>
|
|
* <<Optimize Memory Allocation>>
|
|
* <<Use Persistent Shared Memory>>
|
|
* <<Safely Allow Access from Multiple Processes>>
|
|
* <<Add Serialization to Archive the Database>>
|
|
* <<Next Steps>>
|
|
* <<See Also>>
|
|
|
|
== Libraries
|
|
|
|
Here are some Boost libraries that might be useful when planning and building your database app:
|
|
|
|
[circle]
|
|
* boost:container[] : Provides STL-compatible containers, including stable vector, flat set/map and more. The containers provided by this library can offer performance benefits over their standard library equivalents, making them a good fit for a high-performance database application.
|
|
|
|
* boost:pool[] : This library is used for simple, fast memory allocation and can improve efficiency in some scenarios by managing memory in chunks.
|
|
|
|
* boost:interprocess[] : This library allows for shared memory communication and synchronization between processes. In a database context, this can be useful for inter-process communication (IPC) and shared memory databases.
|
|
|
|
* boost:lockfree[] : Provides lock-free data structures which could be useful in multi-threaded database applications where you want to avoid locking overhead.
|
|
|
|
* boost:serialization[] : If you need to serialize objects for storage, boost:serialization[] can be a useful tool. However, be aware that for many database applications, more specialized serialization formats (like Protocol Buffers, Thrift, etc.) might be more appropriate.
|
|
|
|
* boost:asio[] : Provides a consistent asynchronous model using a modern pass:[C++] approach for network and low-level I/O programming. It supports a variety of network protocols, which could be helpful if your database needs to communicate over a network.
|
|
|
|
* boost:thread[] : Provides a portable interface for multithreading, which can be crucial when creating a high-performance database that can handle multiple queries concurrently.
|
|
|
|
* boost:fiber[] : Allows you to write code that works with fibers, which are user-space threads that can be used to write concurrent code. This can be useful in situations where you have many tasks that need to run concurrently but are I/O-bound rather than CPU-bound.
|
|
|
|
* boost:polygon[] or boost:geometry[] : For storing and querying spatial data, these libraries can provide the necessary data types and algorithms.
|
|
|
|
* boost:filesystem[] : Provides a portable way of querying and manipulating paths, files, and directories.
|
|
|
|
Note:: The code in this tutorial was written and tested using Microsoft Visual Studio (Visual C++ 2022, Console App project) with Boost version 1.88.0.
|
|
|
|
== Sample Database Engine using Containers
|
|
|
|
A database engine requires efficient data structures for handling indexes, caches, and storage layouts. The boost:container[] library provides drop-in replacements for standard containers like `std::vector`, `std::map`, and `std::unordered_map`, but optimized for memory efficiency and performance.
|
|
|
|
In the following sample code, we will use in-memory indexing as the basis of a database engine. The boost:container[] `flat_map` feature is used to store a sorted index for quick lookups, and the `stable_vector` feature to store persistent records with stable pointers. The sample demonstrates inserting and retrieving records efficiently.
|
|
|
|
[source,cpp]
|
|
----
|
|
#include <boost/container/flat_map.hpp>
|
|
#include <boost/container/stable_vector.hpp>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
// Define a Simple Database Table Structure
|
|
struct Record {
|
|
int id; // Primary Key
|
|
std::string name; // Represents record data
|
|
|
|
Record(int id, std::string name) : id(id), name(std::move(name)) {}
|
|
};
|
|
|
|
// Implement a Database Table Class
|
|
class DatabaseTable {
|
|
public:
|
|
using RecordStorage = boost::container::stable_vector<Record>;
|
|
using IndexMap = boost::container::flat_map<int, size_t>; // Fast lookup
|
|
|
|
void insert(int id, const std::string& name) {
|
|
size_t index = records.size();
|
|
records.emplace_back(id, name);
|
|
index_map[id] = index;
|
|
}
|
|
|
|
const Record* find(int id) {
|
|
auto it = index_map.find(id);
|
|
if (it != index_map.end()) {
|
|
return &records[it->second];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void print_all() const {
|
|
for (const auto& record : records) {
|
|
std::cout << "ID: " << record.id << ", Name: " << record.name << "\n";
|
|
}
|
|
}
|
|
|
|
private:
|
|
RecordStorage records; // Stores records in a stable manner
|
|
IndexMap index_map; // Provides fast ID lookups
|
|
};
|
|
|
|
// Demonstrate Database Operations
|
|
int main() {
|
|
DatabaseTable db;
|
|
|
|
// Insert records
|
|
db.insert(101, "Alice");
|
|
db.insert(102, "Bob");
|
|
db.insert(103, "Charlie");
|
|
|
|
// Retrieve a record
|
|
const Record* record = db.find(102);
|
|
if (record) {
|
|
std::cout << "Found: ID = " << record->id << ", Name = " << record->name << "\n";
|
|
} else {
|
|
std::cout << "Record not found!\n";
|
|
}
|
|
|
|
// Print all records
|
|
std::cout << "All records:\n";
|
|
db.print_all();
|
|
|
|
return 0;
|
|
}
|
|
|
|
----
|
|
|
|
Note:: Key features of this sample are that it is memory-efficient (reducing fragmentation and with good performance), `stable_vector` prevents invalid references when resizing, and `flat_map` is faster than `std::map` for heavy use.
|
|
|
|
Run the program, the output should be:
|
|
|
|
[source,text]
|
|
----
|
|
Found: ID = 102, Name = Bob
|
|
All records:
|
|
ID: 101, Name: Alice
|
|
ID: 102, Name: Bob
|
|
ID: 103, Name: Charlie
|
|
|
|
----
|
|
|
|
== Optimize Memory Allocation
|
|
|
|
As we are dealing with frequent allocations of small objects (the database records) we'll enhance our database engine by using boost:pool[]. This library avoids repeated calls to `malloc`, `new` and `delete`.
|
|
|
|
[source,cpp]
|
|
----
|
|
#include <boost/container/flat_map.hpp>
|
|
#include <boost/pool/pool.hpp>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
struct Record {
|
|
int id;
|
|
std::string name;
|
|
|
|
Record(int id, std::string name) : id(id), name(std::move(name)) {}
|
|
};
|
|
|
|
class DatabaseTable {
|
|
public:
|
|
using IndexMap = boost::container::flat_map<int, Record*>;
|
|
|
|
DatabaseTable() : recordPool(sizeof(Record)) {}
|
|
|
|
Record* insert(int id, const std::string& name) {
|
|
void* memory = recordPool.malloc(); // Allocate memory from the pool
|
|
if (!memory) {
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
Record* newRecord = new (memory) Record(id, name); // Placement new
|
|
index_map[id] = newRecord;
|
|
return newRecord;
|
|
}
|
|
|
|
void remove(int id) {
|
|
auto it = index_map.find(id);
|
|
if (it != index_map.end()) {
|
|
it->second->~Record(); // Call destructor
|
|
recordPool.free(it->second); // Free memory back to the pool
|
|
index_map.erase(it);
|
|
}
|
|
}
|
|
|
|
Record* find(int id) {
|
|
auto it = index_map.find(id);
|
|
return (it != index_map.end()) ? it->second : nullptr;
|
|
}
|
|
|
|
void print_all() {
|
|
for (const auto& pair : index_map) {
|
|
std::cout << "ID: " << pair.first << ", Name: " << pair.second->name << "\n";
|
|
}
|
|
}
|
|
|
|
~DatabaseTable() {
|
|
for (const auto& pair : index_map) {
|
|
pair.second->~Record();
|
|
recordPool.free(pair.second);
|
|
}
|
|
}
|
|
|
|
private:
|
|
boost::pool<> recordPool;
|
|
IndexMap index_map;
|
|
};
|
|
|
|
// Demonstrate Efficient Memory Use
|
|
int main() {
|
|
DatabaseTable db;
|
|
|
|
// Insert records
|
|
db.insert(101, "Alice");
|
|
db.insert(102, "Bob");
|
|
db.insert(103, "Charlie");
|
|
|
|
// Retrieve a record
|
|
Record* record = db.find(102);
|
|
if (record) {
|
|
std::cout << "Found: ID = " << record->id << ", Name = " << record->name << "\n";
|
|
}
|
|
|
|
// Remove a record
|
|
db.remove(102);
|
|
if (!db.find(102)) {
|
|
std::cout << "Record 102 removed successfully.\n";
|
|
}
|
|
|
|
// Print all records
|
|
std::cout << "All records:\n";
|
|
db.print_all();
|
|
|
|
return 0;
|
|
}
|
|
|
|
----
|
|
|
|
Note:: Custom _Object Pools_ can be tuned for your specific object sizes.
|
|
|
|
The output should be:
|
|
|
|
[source,text]
|
|
----
|
|
Found: ID = 102, Name = Bob
|
|
Record 102 removed successfully.
|
|
All records:
|
|
ID: 101, Name: Alice
|
|
ID: 103, Name: Charlie
|
|
|
|
----
|
|
|
|
== Use Persistent Shared Memory
|
|
|
|
In a realistic database environment, you would probably want to enable a shared-memory database table that multiple processes can access simultaneously. For this, we need the features of boost:interprocess[]. This library enables multiple processes to share the same data faster than inter-process communication (IPC) via files or sockets, and includes mutexes and condition variables.
|
|
|
|
[source,cpp]
|
|
----
|
|
#include <boost/interprocess/managed_shared_memory.hpp>
|
|
#include <boost/interprocess/containers/vector.hpp>
|
|
#include <iostream>
|
|
|
|
namespace bip = boost::interprocess;
|
|
|
|
const char* SHM_NAME = "SharedDatabase";
|
|
const char* TABLE_NAME = "UserTable";
|
|
const std::size_t MAX_USERS = 10;
|
|
|
|
struct UserRecord {
|
|
int id;
|
|
char name[32];
|
|
};
|
|
|
|
using ShmemAllocator = bip::allocator<UserRecord, bip::managed_shared_memory::segment_manager>;
|
|
using UserTable = bip::vector<UserRecord, ShmemAllocator>;
|
|
|
|
void create_table() {
|
|
bip::shared_memory_object::remove(SHM_NAME);
|
|
|
|
bip::managed_shared_memory segment(bip::create_only, SHM_NAME, 65536);
|
|
const ShmemAllocator alloc_inst(segment.get_segment_manager());
|
|
UserTable* table = segment.construct<UserTable>(TABLE_NAME)(alloc_inst);
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
UserRecord user;
|
|
user.id = 1 + table->size();
|
|
std::snprintf(user.name, sizeof(user.name), "User%d", user.id);
|
|
table->push_back(user);
|
|
}
|
|
|
|
std::cout << "Shared memory table created with 3 initial users.\n";
|
|
}
|
|
|
|
void show_table() {
|
|
|
|
try
|
|
{
|
|
bip::managed_shared_memory segment(bip::open_only, SHM_NAME);
|
|
UserTable* table = segment.find<UserTable>(TABLE_NAME).first;
|
|
|
|
if (!table) {
|
|
std::cerr << "Table not found.\n";
|
|
return;
|
|
}
|
|
|
|
std::cout << "User Table:\n";
|
|
for (const auto& user : *table) {
|
|
std::cout << " ID: " << user.id << ", Name: " << user.name << "\n";
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
std::cerr << "Shared Memory error - create a table\n";
|
|
}
|
|
}
|
|
|
|
void add_user() {
|
|
|
|
try
|
|
{
|
|
bip::managed_shared_memory segment(bip::open_only, SHM_NAME);
|
|
UserTable* table = segment.find<UserTable>(TABLE_NAME).first;
|
|
|
|
if (!table) {
|
|
std::cerr << "Table not found.\n";
|
|
return;
|
|
}
|
|
|
|
if (table->size() >= MAX_USERS) {
|
|
std::cerr << "Table is full (max " << MAX_USERS << " users).\n";
|
|
return;
|
|
}
|
|
|
|
std::string name;
|
|
|
|
std::cout << "Enter user name: ";
|
|
std::getline(std::cin, name);
|
|
|
|
UserRecord user;
|
|
user.id = 1 + table->size();
|
|
std::snprintf(user.name, sizeof(user.name) - 1, "%s", name.c_str());
|
|
user.name[sizeof(user.name) - 1] = '\0';
|
|
|
|
table->push_back(user);
|
|
std::cout << "User added.\n";
|
|
}
|
|
catch (...)
|
|
{
|
|
std::cerr << "Shared Memory error - create a table\n";
|
|
}
|
|
}
|
|
|
|
void print_menu() {
|
|
std::cout << "\n=== Shared Memory User Table Menu ===\n";
|
|
std::cout << "1. Create table 2. Show table 3. Add user 4. Clear shared memory 5. Exit: ";
|
|
}
|
|
|
|
int main() {
|
|
while (true) {
|
|
print_menu();
|
|
|
|
int choice = 0;
|
|
std::cin >> choice;
|
|
std::cin.ignore(); // discard newline
|
|
|
|
switch (choice) {
|
|
case 1:
|
|
create_table();
|
|
show_table();
|
|
break;
|
|
case 2:
|
|
show_table();
|
|
break;
|
|
case 3:
|
|
add_user();
|
|
break;
|
|
case 4:
|
|
bip::shared_memory_object::remove(SHM_NAME);
|
|
break;
|
|
case 5:
|
|
std::cout << "Exiting...\n";
|
|
return 0;
|
|
default:
|
|
std::cout << "Invalid option. Try again.\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
----
|
|
|
|
Boost shared memory is persistent. Run the program, add some user records, and exit without choosing option `4`. Then run the program again and note the records you added have persisted.
|
|
|
|
First run:
|
|
|
|
[source,text]
|
|
----
|
|
=== Shared Memory User Table Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Clear shared memory 5. Exit: 1
|
|
Shared memory table created with 3 initial users.
|
|
User Table:
|
|
ID: 1, Name: User1
|
|
ID: 2, Name: User2
|
|
ID: 3, Name: User3
|
|
|
|
=== Shared Memory User Table Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Clear shared memory 5. Exit: 3
|
|
Enter user name: Nigel
|
|
User added.
|
|
|
|
=== Shared Memory User Table Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Clear shared memory 5. Exit: 2
|
|
User Table:
|
|
ID: 1, Name: User1
|
|
ID: 2, Name: User2
|
|
ID: 3, Name: User3
|
|
ID: 4, Name: Nigel
|
|
|
|
=== Shared Memory User Table Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Clear shared memory 5. Exit: 5
|
|
Exiting...
|
|
----
|
|
|
|
Second run:
|
|
|
|
[source,text]
|
|
----
|
|
=== Shared Memory User Table Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Clear shared memory 5. Exit: 2
|
|
User Table:
|
|
ID: 1, Name: User1
|
|
ID: 2, Name: User2
|
|
ID: 3, Name: User3
|
|
ID: 4, Name: Nigel
|
|
----
|
|
|
|
== Safely Allow Access from Multiple Processes
|
|
|
|
To safely allow multiple processes to access and modify shared memory concurrently in your boost:interprocess[] program, you should use interprocess synchronization primitives — like `interprocess_mutex` to guard critical sections.
|
|
|
|
|
|
[source,cpp]
|
|
----
|
|
#include <boost/interprocess/managed_shared_memory.hpp>
|
|
#include <boost/interprocess/containers/vector.hpp>
|
|
#include <iostream>
|
|
|
|
namespace bip = boost::interprocess;
|
|
|
|
const char* SHM_NAME = "SharedDatabase";
|
|
const std::size_t MAX_USERS = 10;
|
|
|
|
struct UserRecord {
|
|
int id;
|
|
char name[32];
|
|
};
|
|
|
|
using SegmentManager = bip::managed_shared_memory::segment_manager;
|
|
using ShmemAllocator = bip::allocator<UserRecord, SegmentManager>;
|
|
using UserTable = bip::vector<UserRecord, ShmemAllocator>;
|
|
|
|
// Wrap the shared data and the mutex
|
|
struct SharedData {
|
|
bip::interprocess_mutex mutex;
|
|
UserTable table;
|
|
|
|
SharedData(const ShmemAllocator& alloc) : table(alloc) {}
|
|
};
|
|
|
|
const char* TABLE_NAME = "SharedUserTable";
|
|
|
|
void create_table() {
|
|
bip::shared_memory_object::remove(SHM_NAME);
|
|
|
|
bip::managed_shared_memory segment(bip::create_only, SHM_NAME, 65536);
|
|
ShmemAllocator alloc_inst(segment.get_segment_manager());
|
|
|
|
// Construct SharedData in shared memory
|
|
segment.construct<SharedData>(TABLE_NAME)(alloc_inst);
|
|
|
|
std::cout << "Shared memory table created.\n";
|
|
}
|
|
|
|
void show_table() {
|
|
try {
|
|
bip::managed_shared_memory segment(bip::open_only, SHM_NAME);
|
|
SharedData* data = segment.find<SharedData>(TABLE_NAME).first;
|
|
if (!data) {
|
|
std::cerr << "Table not found.\n";
|
|
return;
|
|
}
|
|
|
|
bip::scoped_lock<bip::interprocess_mutex> lock(data->mutex);
|
|
std::cout << "User Table:\n";
|
|
for (const auto& user : data->table) {
|
|
std::cout << " ID: " << user.id << ", Name: " << user.name << "\n";
|
|
}
|
|
}
|
|
catch (...) {
|
|
std::cerr << "Error accessing shared memory. Is it created?\n";
|
|
}
|
|
}
|
|
|
|
void add_user() {
|
|
try {
|
|
bip::managed_shared_memory segment(bip::open_only, SHM_NAME);
|
|
SharedData* data = segment.find<SharedData>(TABLE_NAME).first;
|
|
if (!data) {
|
|
std::cerr << "Table not found.\n";
|
|
return;
|
|
}
|
|
|
|
bip::scoped_lock<bip::interprocess_mutex> lock(data->mutex);
|
|
|
|
if (data->table.size() >= MAX_USERS) {
|
|
std::cerr << "Table is full (max " << MAX_USERS << " users).\n";
|
|
return;
|
|
}
|
|
|
|
std::string name;
|
|
std::cout << "Enter user name: ";
|
|
std::cin.ignore();
|
|
std::getline(std::cin, name);
|
|
|
|
UserRecord user;
|
|
user.id = 1 + static_cast<int>(data->table.size());
|
|
std::snprintf(user.name, sizeof(user.name) - 1, "%s", name.c_str());
|
|
user.name[sizeof(user.name) - 1] = '\0';
|
|
|
|
data->table.push_back(user);
|
|
std::cout << "User added.\n";
|
|
|
|
}
|
|
catch (...) {
|
|
std::cerr << "Error accessing shared memory. Is it created?\n";
|
|
}
|
|
}
|
|
|
|
void print_menu() {
|
|
std::cout << "\n=== Shared Memory User Table Menu ===\n";
|
|
std::cout << "1. Create table 2. Show table 3. Add user 4. Clear shared memory 5. Exit\n";
|
|
std::cout << "Choose an option: ";
|
|
}
|
|
|
|
int main() {
|
|
while (true) {
|
|
print_menu();
|
|
|
|
int choice = 0;
|
|
std::cin >> choice;
|
|
|
|
switch (choice) {
|
|
case 1:
|
|
create_table();
|
|
show_table();
|
|
break;
|
|
case 2:
|
|
show_table();
|
|
break;
|
|
case 3:
|
|
add_user();
|
|
break;
|
|
case 4:
|
|
bip::shared_memory_object::remove(SHM_NAME);
|
|
std::cout << "Shared memory cleared.\n";
|
|
break;
|
|
case 5:
|
|
std::cout << "Exiting...\n";
|
|
return 0;
|
|
default:
|
|
std::cout << "Invalid option. Try again.\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
----
|
|
|
|
Now it is safe to run this program from two, or more, terminal sessions.
|
|
|
|
== Add Serialization to Archive the Database
|
|
|
|
Finally, let's add the features of boost:serialization[] to allow us to save and restore snapshots of our shared-memory database, making it persistent across program runs even when the shared memory is cleared. We will extend our sample to serialize the records into an archive format.
|
|
|
|
[source,cpp]
|
|
----
|
|
#include <boost/interprocess/managed_shared_memory.hpp> // For managing shared memory segments
|
|
#include <boost/interprocess/containers/vector.hpp> // STL-like vector that works inside shared memory
|
|
#include <boost/interprocess/sync/named_mutex.hpp> // Mutex across processes
|
|
#include <boost/serialization/vector.hpp> // Serialization support for std::vector
|
|
#include <boost/archive/text_oarchive.hpp> // For saving serialized data to text files
|
|
#include <boost/archive/text_iarchive.hpp> // For loading serialized data from text files
|
|
#include <iostream>
|
|
#include <fstream>
|
|
|
|
namespace bip = boost::interprocess;
|
|
|
|
// ---- Global configuration constants ----
|
|
const char* SHM_NAME = "SharedDatabase"; // Name of the shared memory segment
|
|
const char* TABLE_NAME = "UserTable"; // Name of the container object inside shared memory
|
|
const char* MUTEX_NAME = "SharedTableMutex"; // Name of the interprocess mutex
|
|
const std::size_t MAX_USERS = 10; // Maximum number of users allowed in table
|
|
|
|
// ---- User Record structure, supports Boost.Serialization ----
|
|
struct UserRecord {
|
|
int id; // Unique user ID
|
|
char name[32]; // Fixed-size character buffer for username
|
|
|
|
// Serialization function used by Boost.Archive
|
|
template<class Archive>
|
|
void serialize(Archive& ar, const unsigned int) {
|
|
ar& id;
|
|
|
|
// Wrap raw array in make_array so Boost knows how to handle it
|
|
ar& boost::serialization::make_array(name, sizeof(name));
|
|
}
|
|
};
|
|
|
|
// ---- Type aliases for clarity ----
|
|
using ShmemAllocator = bip::allocator<UserRecord, bip::managed_shared_memory::segment_manager>;
|
|
|
|
// Vector of UserRecords in shared memory
|
|
using UserTable = bip::vector<UserRecord, ShmemAllocator>;
|
|
|
|
// ---- Create a new table in shared memory ----
|
|
void create_table() {
|
|
|
|
// Remove any old shared memory segment and mutex (cleanup)
|
|
bip::shared_memory_object::remove(SHM_NAME);
|
|
bip::named_mutex::remove(MUTEX_NAME);
|
|
|
|
// Create new shared memory segment of fixed size (64 KB here)
|
|
bip::managed_shared_memory segment(bip::create_only, SHM_NAME, 65536);
|
|
ShmemAllocator alloc(segment.get_segment_manager());
|
|
|
|
// Construct a UserTable object inside shared memory
|
|
UserTable* table = segment.construct<UserTable>(TABLE_NAME)(alloc);
|
|
|
|
// Pre-populate with three sample users
|
|
for (int i = 0; i < 3; ++i) {
|
|
UserRecord user;
|
|
user.id = 1 + table->size();
|
|
std::snprintf(user.name, sizeof(user.name), "User%d", user.id);
|
|
table->push_back(user);
|
|
}
|
|
|
|
std::cout << "Shared memory table created with 3 initial users.\n";
|
|
}
|
|
|
|
// ---- Display the contents of the table ----
|
|
void show_table() {
|
|
try {
|
|
bip::managed_shared_memory segment(bip::open_only, SHM_NAME);
|
|
bip::named_mutex mutex(bip::open_or_create, MUTEX_NAME);
|
|
|
|
// Lock table to prevent concurrent modifications
|
|
bip::scoped_lock<bip::named_mutex> lock(mutex);
|
|
|
|
// Find UserTable in shared memory
|
|
UserTable* table = segment.find<UserTable>(TABLE_NAME).first;
|
|
if (!table) {
|
|
std::cerr << "Table not found.\n";
|
|
return;
|
|
}
|
|
|
|
// Print all users
|
|
std::cout << "User Table:\n";
|
|
for (const auto& user : *table) {
|
|
std::cout << " ID: " << user.id << ", Name: " << user.name << "\n";
|
|
}
|
|
}
|
|
catch (...) {
|
|
std::cerr << "Unable to access shared memory.\n";
|
|
}
|
|
}
|
|
|
|
// ---- Add a user to the shared memory table ----
|
|
void add_user() {
|
|
try {
|
|
bip::managed_shared_memory segment(bip::open_only, SHM_NAME);
|
|
bip::named_mutex mutex(bip::open_or_create, MUTEX_NAME);
|
|
bip::scoped_lock<bip::named_mutex> lock(mutex);
|
|
|
|
UserTable* table = segment.find<UserTable>(TABLE_NAME).first;
|
|
if (!table || table->size() >= MAX_USERS) {
|
|
std::cerr << "Table not found or full.\n";
|
|
return;
|
|
}
|
|
|
|
// Get new user name from console
|
|
std::string name;
|
|
|
|
// Discard leftover newline from previous input
|
|
std::cin.ignore();
|
|
|
|
std::cout << "Enter user name: ";
|
|
std::getline(std::cin, name);
|
|
|
|
// Create new record and append
|
|
UserRecord user;
|
|
user.id = 1 + table->size();
|
|
std::snprintf(user.name, sizeof(user.name) - 1, "%s", name.c_str());
|
|
table->push_back(user);
|
|
|
|
std::cout << "User added.\n";
|
|
}
|
|
catch (...) {
|
|
std::cerr << "Failed to add user.\n";
|
|
}
|
|
}
|
|
|
|
// ---- Save snapshot of current table to a text file ----
|
|
void save_snapshot(const std::string& filename) {
|
|
try {
|
|
bip::managed_shared_memory segment(bip::open_only, SHM_NAME);
|
|
bip::named_mutex mutex(bip::open_or_create, MUTEX_NAME);
|
|
bip::scoped_lock<bip::named_mutex> lock(mutex);
|
|
|
|
UserTable* table = segment.find<UserTable>(TABLE_NAME).first;
|
|
if (!table) {
|
|
std::cerr << "Table not found.\n";
|
|
return;
|
|
}
|
|
|
|
// Copy data from shared memory into std::vector (heap memory)
|
|
std::vector<UserRecord> snapshot(table->begin(), table->end());
|
|
|
|
// Save serialized snapshot to file
|
|
std::ofstream ofs(filename);
|
|
boost::archive::text_oarchive oa(ofs);
|
|
oa << snapshot;
|
|
|
|
std::cout << "Snapshot saved to " << filename << "\n";
|
|
}
|
|
catch (...) {
|
|
std::cerr << "Failed to save snapshot.\n";
|
|
}
|
|
}
|
|
|
|
// ---- Load snapshot from text file into shared memory ----
|
|
void load_snapshot(const std::string& filename) {
|
|
try {
|
|
|
|
// Open file and load into vector
|
|
std::ifstream ifs(filename);
|
|
if (!ifs) {
|
|
std::cerr << "Snapshot file not found.\n";
|
|
return;
|
|
}
|
|
|
|
std::vector<UserRecord> snapshot;
|
|
boost::archive::text_iarchive ia(ifs);
|
|
ia >> snapshot;
|
|
|
|
// Reset shared memory segment and mutex
|
|
bip::shared_memory_object::remove(SHM_NAME);
|
|
bip::managed_shared_memory segment(bip::create_only, SHM_NAME, 65536);
|
|
bip::named_mutex::remove(MUTEX_NAME);
|
|
bip::named_mutex mutex(bip::create_only, MUTEX_NAME);
|
|
bip::scoped_lock<bip::named_mutex> lock(mutex);
|
|
|
|
// Recreate UserTable and repopulate
|
|
ShmemAllocator alloc(segment.get_segment_manager());
|
|
UserTable* table = segment.construct<UserTable>(TABLE_NAME)(alloc);
|
|
|
|
for (const auto& user : snapshot) {
|
|
table->push_back(user);
|
|
}
|
|
|
|
std::cout << "Snapshot loaded from " << filename << "\n";
|
|
}
|
|
catch (...) {
|
|
std::cerr << "Failed to load snapshot.\n";
|
|
}
|
|
}
|
|
|
|
// ---- Clear all shared memory resources ----
|
|
void clear_shared_memory() {
|
|
bip::shared_memory_object::remove(SHM_NAME);
|
|
bip::named_mutex::remove(MUTEX_NAME);
|
|
std::cout << "Shared memory cleared.\n";
|
|
}
|
|
|
|
// ---- Print the interactive menu ----
|
|
void print_menu() {
|
|
std::cout << "\n=== Shared Memory Menu ===\n"
|
|
<< "1. Create table 2. Show table 3. Add user 4. Save snapshot 5. Load snapshot 6. Clear shared memory 7. Exit:";
|
|
}
|
|
|
|
// ---- Program entry point ----
|
|
int main() {
|
|
while (true) {
|
|
print_menu();
|
|
int choice;
|
|
std::cin >> choice;
|
|
|
|
switch (choice) {
|
|
case 1:
|
|
create_table();
|
|
|
|
// Show immediately after creation
|
|
show_table();
|
|
break;
|
|
case 2: show_table(); break;
|
|
case 3: add_user(); break;
|
|
case 4: save_snapshot("snapshot.txt"); break;
|
|
case 5:
|
|
load_snapshot("snapshot.txt");
|
|
show_table();
|
|
break;
|
|
case 6: clear_shared_memory(); break;
|
|
case 7: return 0;
|
|
default: std::cout << "Invalid choice.\n";
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
Run the sample, and verify that the saved file persists after shared memory has been cleared.
|
|
|
|
[source,text]
|
|
----
|
|
=== Shared Memory Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Save snapshot 5. Load snapshot 6. Clear shared memory 7. Exit:1
|
|
Shared memory table created with 3 initial users.
|
|
User Table:
|
|
ID: 1, Name: User1
|
|
ID: 2, Name: User2
|
|
ID: 3, Name: User3
|
|
|
|
=== Shared Memory Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Save snapshot 5. Load snapshot 6. Clear shared memory 7. Exit:3
|
|
Enter user name: Nigel
|
|
User added.
|
|
|
|
=== Shared Memory Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Save snapshot 5. Load snapshot 6. Clear shared memory 7. Exit:4
|
|
Snapshot saved to snapshot.txt
|
|
|
|
=== Shared Memory Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Save snapshot 5. Load snapshot 6. Clear shared memory 7. Exit:6
|
|
Shared memory cleared.
|
|
|
|
=== Shared Memory Menu ===
|
|
1. Create table 2. Show table 3. Add user 4. Save snapshot 5. Load snapshot 6. Clear shared memory 7. Exit:5
|
|
Snapshot loaded from snapshot.txt
|
|
User Table:
|
|
ID: 1, Name: User1
|
|
ID: 2, Name: User2
|
|
ID: 3, Name: User3
|
|
ID: 4, Name: Nigel
|
|
|
|
----
|
|
|
|
== Next Steps
|
|
|
|
In the design of a database, consider all the independent processes, and how they might access persistent memory, for example:
|
|
|
|
image::database-persistent-memory.png[]
|
|
|
|
Perhaps now consider boost:filesystem[] for file management, and for a heavier duty database engine - integrate boost:asio[] to handle remote database transactions. Referring to the xref:task-networking.adoc[] sample would be a good place to start.
|
|
|
|
The Boost libraries have a lot to offer this particular scenario!
|
|
|
|
== See Also
|
|
|
|
* https://www.boost.org/doc/libs/latest/libs/libraries.htm#Containers[Category: Containers]
|
|
* https://www.boost.org/doc/libs/latest/libs/libraries.htm#Data[Category: Data structures]
|
|
* https://www.boost.org/doc/libs/latest/libs/libraries.htm#Memory[Category: Memory]
|
|
|