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
lock
argument is used. This means there is no need to specifysync
ormode(sync)
per field if it is already locked. - The
lock
argument itself can be implicit whenreader
orwriter
arguments are used. Apparently, thesync
mode 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.