Locks
Support for sync/async modes of operation is incomplete without providing a way to lock-protect data. FieldX can do this for you at the field level by using the lock argument.
Using inner_mut with explicit sync or mode(sync) has the same effect as using the lock argument.
For greater flexibility, FieldX utilizes read-write locks provided by the parking_lot crate for the sync mode, and either tokio or async-lock for the async mode, depending on which feature flag is enabled. This design decision aligns well with the immutable and mutable accessor patterns.
Making a field lockable affects the return values of its accessors. Without focusing on specific sync or async modes, the immutable accessor of such a field returns an RwLockReadGuard, while the mutable accessor returns an RwLockWriteGuard:
#[rustfmt::skip]
#[fxstruct(get, builder(into))]
struct Book {
title: String,
author: String,
#[fieldx(get(copy), builder(into(off)))]
year: u32,
#[fieldx(optional)]
signed_by: String,
location: String,
#[fieldx(reader, writer, get(copy), get_mut, builder(into(off)))]
available: u32,
// Map borrower IDs to the time they borrowed the book.
#[fieldx(lock, get_mut, builder(off))]
borrowers: HashMap<String, Instant>,
}
let book = Book::builder()
.title("The Catcher in the Rye")
.author("J.D. Salinger")
.year(1951)
.signed_by("S.K.")
.location("R42.S1".to_string()) // Row 12, Section 2
.available(50)
.build()
.expect("Failed to create Book object");
let borrowers = 30;
let barrier = std::sync::Barrier::new(borrowers + 1);
thread::scope(|s| {
for i in 0..borrowers {
let book_ref = &book;
let idx = i;
let barrier = &barrier;
s.spawn(move || {
// Try to ensure real concurrency by making all threads start at the same time.
barrier.wait();
*book_ref.write_available() -= 1;
book_ref.borrowers_mut().insert(format!("user{idx}"), Instant::now());
});
}
barrier.wait();
});
assert_eq!(book.available(), 20);
assert_eq!(*book.read_available(), 20);
assert_eq!(book.borrowers().len(), borrowers as usize);
The use of the struct and its locked fields is straightforward in the example above. What is really worth mentioning are the following two points:
- Implicit sync mode when the
lockargument is used. This means there is no need to specifysyncormode(sync)per field if it is already locked. - The
lockargument itself can be implicit whenreaderorwriterarguments are used. Apparently, thesyncmode is also implied in this case.
Readers And Writers
The reader and writer arguments are additional helpers that introduce methods always returning an RwLockReadGuard and RwLockWriteGuard, respectively. The methods are named after the field they are applied to, with the read_ and write_ prefixes added.
At this point, an immediate question arises: what makes them different from the immutable and mutable accessors? For the reader, the answer is the word "always" in the previous paragraph. Consider the copy subargument of the available field's get – with it, the locking happens inside the accessor, and we only get a u32 value.
For the writer, the situation is different and slightly more intricate. It will be discussed in the Lock'n'Lazy chapter.