I’m trying to write a structure, a PageManager, that can hold up to a fixed number of pages where each page will be about 4K in size. I want to be able to access both the page manager and the pages stored within from multiple threads. A page can either be shared to multiple threads for reading or to one thread for writing. The standard Rust reference pattern.
The page manager does four things. You can insert a page, you can remove a page, and you can get a read or write reference to any of the pages in the page manager. I have come up with a solution that works ( I think) , but I have this feeling there must be a better way to do this.
I have tried just storing the pages in a HashMap but I couldn’t get that to work. Having a mutable reference to the page manager would let me insert or remove pages, but I think I can only do that once I have dropped all read and write locks on all pages. Wrapping the HashMap in a RwLock would not let me return the stored page but instead gave me error E0515, cannot return value referencing temporary value.
What I came up with was to allocate a vector where I can store a fixed number of pages and add empty pages in all slots. When inserting a page I find the first empty slot, or a page that is no longer in use and replace the page currently in the slot with the new page. Removing a page is replacing the page currently in the slot with an empty page. Now I can use a HashMap to store which page id points to which index in the vector, since the usize can be copied.
Here is a bare bones version of what I did.
I guess my question is how should I be doing this?
use std::collections::HashMap;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::time::{Duration};
struct Page {
buffer: Vec<u8>
}
impl Page {
fn new(size: usize) -> Self {
Page {
buffer: vec![0; size]
}
}
}
struct PageManger {
page_map: RwLock<HashMap<u64, usize>>,
/// The pages are stored in a fixed sized vector which will allow us to insert a page into an
/// empty slot and remove a page as long as the page isn't in use.
pages: Vec<Arc<RwLock<Page>>>
}
impl PageManger {
fn new(capacity: usize) -> Self {
let mut pages = Vec::new();
for _ in 0..capacity {
pages.push(Arc::new(RwLock::new(Page::new(0))));
}
PageManger {
page_map: RwLock::new(HashMap::new()),
pages
}
}
fn insert_page(&self, page_id: u64, page: Page) {
// First we try and insert the page into an empty slot.
for (i, p) in self.pages.iter().enumerate() {
match p.try_write() {
Ok(mut marker) => {
if marker.buffer.len() > 0 {
continue;
}
*marker = page;
let mut page_map = self.page_map.write().unwrap();
page_map.insert(page_id, i);
return;
}
Err(_) => todo!()
}
}
// If we didn't find an empty slot we replace the first page that allow us to get a write lock.
for (i, p) in self.pages.iter().enumerate() {
match p.try_write() {
Ok(mut marker) => {
*marker = page;
let mut page_map = self.page_map.write().unwrap();
page_map.insert(page_id, i);
return;
}
Err(_) => todo!()
}
}
// TODO: Error handling
}
fn remove_page(&self, page_id: u64) {
let mut marker = self.get_page_mut(page_id);
*marker = Page::new(0);
}
fn get_page(&self, page_id: u64) -> RwLockReadGuard<'_, Page> {
let pos = self.page_map.read().unwrap().get(&page_id).copied().unwrap();
self.pages[pos].read().unwrap()
}
fn get_page_mut(&self, page_id: u64) -> RwLockWriteGuard<'_, Page> {
let pos = self.page_map.read().unwrap().get(&page_id).copied().unwrap();
self.pages[pos].write().unwrap()
}
}
fn main() {
let page_manager = PageManger::new(3);
page_manager.insert_page(100, Page::new(64));
page_manager.insert_page(200, Page::new(64));
let page_manager = Arc::new(page_manager);
let mut handles = Vec::new();
let pm = page_manager.clone();
handles.push(std::thread::spawn(move ||{
let p0 = pm.get_page(100);
std::thread::sleep(Duration::from_millis(1500));
}));
let pm = page_manager.clone();
handles.push(std::thread::spawn(move ||{
let mut p1 = pm.get_page_mut(200);
std::thread::sleep(Duration::from_millis(500));
p1.buffer[0] = 1;
}));
let pm = page_manager.clone();
handles.push(std::thread::spawn(move ||{
std::thread::sleep(Duration::from_millis(100));
let p1 = pm.get_page(200);
assert_eq!(p1.buffer[0], 1);
}));
let pm = page_manager.clone();
handles.push(std::thread::spawn(move ||{
std::thread::sleep(Duration::from_millis(10));
pm.remove_page(100);
pm.insert_page(100, Page::new(4096));
}));
for handle in handles {
handle.join().unwrap();
}
}