Expand description
Mechanisms for creating families of linked objects that can collaborate across threads while being internally single-threaded.
The linked object pattern ensures that cross-thread state sharing is always explicit, as well as cross-thread transfer of linked object instances, facilitated by the mechanisms in this crate. Each individual instance of a linked object and the mechanisms for obtaining new instances are structured in a manner that helps avoid accidental or implicit shared state, by making each instance thread-local while the entire family can act together to provide a multithreaded API to user code.
This is part of the Folo project that provides mechanisms for high-performance hardware-aware programming in Rust.
§Definitions
Linked objects are types whose instances:
- are each local to a single thread (i.e.
!Send
); - and are internally connected to other instances from the same family;
- and share some thread-safe state via messaging or synchronized state;
- and perform all collaboration between instances without involvement of user code (i.e. there is
no
Arc
orMutex
that the user needs to create).
Note that despite instances of linked objects being thread-local (!Send
), there may still be
multiple instances per thread. You can explicitly opt-in to “one per thread” behavior via the
linked::PerThread<T>
wrapper.
Instances belong to the same family if they:
- are created via cloning;
- or are created by obtaining a thread-safe Handle and converting it to a new instance;
- or are obtained from the same static variable in a
linked::instance_per_access!
orlinked::instance_per_thread!
macro block; - or are created from the same
linked::PerThread<T>
or one of its clones.
§Using and defining linked objects
A very basic and contrived example is a Thing
that shares a value
between all its instances.
This object can generally be used like any other Rust type. All linked objects support cloning, since that is one of the primary mechanisms for creating additional linked instances.
let thing1 = Thing::new("hello".to_string());
let thing2 = thing1.clone();
assert_eq!(thing1.value(), "hello");
assert_eq!(thing2.value(), "hello");
thing1.set_value("world".to_string());
assert_eq!(thing1.value(), "world");
assert_eq!(thing2.value(), "world");
We can compare this example to the linked object definition above:
- The relation between instances is established via cloning.
- The
value
is shared. - Implementing the collaboration does not require anything (e.g. a
Mutex
) from user code.
The implementation of this type is the following:
use std::sync::{Arc, Mutex};
#[linked::object]
pub struct Thing {
value: Arc<Mutex<String>>,
}
impl Thing {
pub fn new(initial_value: String) -> Self {
let shared_value = Arc::new(Mutex::new(initial_value));
linked::new!(Self {
value: Arc::clone(&shared_value),
})
}
pub fn value(&self) -> String {
self.value.lock().unwrap().clone()
}
pub fn set_value(&self, value: String) {
*self.value.lock().unwrap() = value;
}
}
The implementation steps to apply the pattern to a struct are:
- Apply
#[linked::object]
on the struct. This will automatically derive thelinked::Object
andClone
traits and implement various other behind-the-scenes mechanisms required for the linked object pattern to operate. - In the constructor, call
linked::new!
to create the first instance.
linked::new!
is a wrapper around a Self
struct-expression. What makes
it special is that it will be called for every instance that is ever created in the same family
of linked objects. This expression captures the state of the constructor (e.g. in the above
example, it captures shared_value
). Use the captured state to set up any shared connections
between instances (e.g. by sharing an Arc
or connecting message channels).
The captured values must be thread-safe (Send
+ Sync
+ 'static
), while the Thing
struct
itself does not need to be thread-safe. In fact, the linked object pattern forces it to be !Send
and !Sync
to avoid accidental multithreading. See the next chapter to understand how to deal with
multithreaded logic.
§Linked objects on multiple threads
Each instance of a linked object is single-threaded (enforced at compile time). To create a
related instance on a different thread, you must either use a static variable inside a
linked::instance_per_access!
or linked::instance_per_thread!
block or
obtain a Handle that you can transfer to another thread and use to obtain a new instance there.
Linked object handles are thread-safe.
Example of using a static variable to connect instances on different threads:
use std::thread;
linked::instance_per_access!(static THE_THING: Thing = Thing::new("hello".to_string()));
let thing = THE_THING.get();
assert_eq!(thing.value(), "hello");
thing.set_value("world".to_string());
thread::spawn(|| {
let thing = THE_THING.get();
assert_eq!(thing.value(), "world");
}).join().unwrap();
Example of using a Handle to transfer an instance to another thread:
use linked::Object; // This brings .handle() into scope.
use std::thread;
let thing = Thing::new("hello".to_string());
assert_eq!(thing.value(), "hello");
thing.set_value("world".to_string());
let thing_handle = thing.handle();
thread::spawn(|| {
let thing: Thing = thing_handle.into();
assert_eq!(thing.value(), "world");
}).join().unwrap();
§Using linked objects via abstractions
You may find yourself in a situation where you need to use a linked object type T
through
a trait object of a trait Xyz
that T
implements, as dyn Xyz
. This is a common pattern
in Rust but with the linked objects pattern there is a choice you must make:
- If the linked objects are always to be accessed via trait objects, wrap the instances in
linked::Box
, returning such a box already in the constructor. - If the linked objects are sometimes to be accessed via trait objects, you can on-demand
wrap them into a
Box<dyn Xyz>
.
The difference is that linked::Box
preserves the linked object functionality - you can
clone the box, obtain a Handle<linked::Box<dyn Xyz>>
to transfer the box to another
thread and store such a box in a static variable in a linked::instance_per_access!
or
linked::instance_per_thread!
block. However, when you use a
Box<dyn Xyz>
, you lose the linked object functionality (but only for the
instance that you put in the box).
impl XmlConfig {
pub fn new_as_config_source() -> linked::Box<dyn ConfigSource> {
linked::new_box!(
dyn ConfigSource,
Self {
config: "xml".to_string(),
}
)
}
}
§Additional examples
See examples/linked_*.rs
for more examples of using linked objects in different scenarios.
Macros§
- instance_
per_ access - Declares that all static variables within the macro body contain linked objects, with each access to this variable returning a new instance from the same family.
- instance_
per_ thread - Declares that all static variables within the macro body contain linked objects, with a single instance from the same family maintained per-thread (at least by this macro - user code may still create additional instances via cloning).
- new
- Defines the template used to create every instance in a linked object family.
- new_box
- Shorthand macro for creating a new
linked::Box
instance, as the exact syntax for that can be cumbersome. This macro is meant to be used in the context of creating a new instance of a linked objectT
that is meant to be always expressed via an abstraction (dyn SomeTrait
).
Structs§
- Box
- A linked object that acts like a
Box<T>
over linked instances ofT
. This is primarily meant to be used with theT
being a trait object, for types exposed to user code via trait objects aslinked::Box<dyn MyTrait>
. - Handle
- A handle can be obtained from any instance of a linked object and used to create new instances from the same family on any thread.
- PerAccess
Static - This is the real type of variables wrapped in the
linked::instance_per_access!
macro. See macro documentation for more details. - PerThread
- A wrapper that manages instances of linked objects of type
T
, ensuring that only one instance ofT
is created per thread. - PerThread
Static - This is the real type of variables wrapped in the
linked::instance_per_thread!
macro. See macro documentation for more details. - Thread
Local - A thread-local instance of a linked object of type
T
. This acts in a manner similar toRc<T>
for a typeT
that implements the linked object pattern. For details, seePerThread<T>
which is the type used to create instances ofThreadLocal<T>
.
Traits§
- Object
- Operations available on every instance of a linked object.
Attribute Macros§
- object
- Marks a struct as implementing the linked object pattern.