Sync feature

The sync feature uses Arc<RwLock<T>> in place of Rc<RefCell<T>> for Res
and State. This allows state and resource objects to be thread safe.

Added threading example

Added threading documentation
This commit is contained in:
Joe Bellus 2024-04-19 19:43:42 -04:00
parent 3b099bfbb0
commit 5896818dbd
8 changed files with 197 additions and 6 deletions

View File

@ -10,5 +10,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
- name: test base
run: cargo test
- name: test sync
run: cargo test --features="sync"

View File

@ -42,7 +42,17 @@ path = "examples/navigation.rs"
name = "stack"
path = "examples/stack.rs"
[[example]]
name = "external"
path = "examples/threading.rs"
required-features = ["sync"]
[dependencies]
anyhow = "1.0.71"
crossterm = "0.27"
ctrlc = "3.3.1"
[features]
sync = []
default = []

View File

@ -1,5 +1,5 @@
# Project name
name: Arkham CLI Library
name: Arkham
# The author displayed on the title page during PDF generation
author: ~
# The path relative to the project root where the compiled static site files

1
docs/guides/group.yml Normal file
View File

@ -0,0 +1 @@
menu_position: 1

75
docs/guides/threading.md Normal file
View File

@ -0,0 +1,75 @@
---
title: Threading
subtitle: Guides
---
# The _sync_ flag
You can enable the sync flag in your _cargo.toml_ file by changing the Arkham deceleration to:
```Toml
arkham = { version = "*", features=["sync"] }
```
With the _sync_ flag enabled `Res` and `State` will be thread safe. This makes it easy to pass the application state or resources to other threads for processing.
# Render signals
When manipulating data from outside of components, especially in other threads, it is useful to be able to notify the app instance that it needs to render changes to the screen. a `Renderer` provides the ability to signal the app instance that it needs to render.
```Rust
let mut app = App::new(root_view);
let renderer = app.get_renderer();
std::thread::spawn(move || loop {
renderer.render()
std::thread::sleep(
std::time::Duration::from_secs(10)
);
});
app.run();
```
# Full threading example
```Rust
use arkham::prelude::*;
#[derive(Default)]
pub struct AppState {
pub counter: i32,
}
fn main() {
let app_state = State::new(AppState::default());
let mut app = App::new(root_view)
.bind_state(app_state.clone());
let renderer = app.get_renderer();
std::thread::spawn(move || loop {
app_state.get_mut().counter += 1;
renderer.render();
std::thread::sleep(
std::time::Duration::from_secs(1)
);
});
app.run().unwrap();
}
fn root_view(
ctx: &mut ViewContext,
state: State<AppState>
) {
ctx.insert(
0,
format!("Count is {}", state.get().counter)
);
}
```

24
examples/threading.rs Normal file
View File

@ -0,0 +1,24 @@
use arkham::prelude::*;
#[derive(Default)]
pub struct AppState {
pub counter: i32,
}
fn main() {
let app_state = State::new(AppState::default());
let mut app = App::new(root_view).bind_state(app_state.clone());
let renderer = app.get_renderer();
std::thread::spawn(move || loop {
app_state.get_mut().counter += 1;
renderer.render();
std::thread::sleep(std::time::Duration::from_secs(1));
});
app.run().unwrap();
}
fn root_view(ctx: &mut ViewContext, state: State<AppState>) {
ctx.insert(0, format!("Count is {}", state.get().counter));
}

View File

@ -117,7 +117,14 @@ where
/// Alternatively, App::insert_state can be used to insert a state object,
/// that can be borrowed mutable.
pub fn insert_resource<T: Any>(self, v: T) -> Self {
self.container.borrow_mut().bind(Res::new(v));
self.bind_resource(Res::new(v))
}
/// Bind an existing resource to the application
///
/// Similar to `App::insert_resource` except it accepts an existing resource.
pub fn bind_resource<T: Any>(self, v: Res<T>) -> Self {
self.container.borrow_mut().bind(v);
self
}
@ -142,7 +149,14 @@ where
/// }
/// ````
pub fn insert_state<T: Any>(self, v: T) -> Self {
self.container.borrow_mut().bind(State::new(v));
self.bind_state(State::new(v))
}
/// Binds an existing state to the application.
///
/// Similar to `App::insert_state` but will accept an existing state
pub fn bind_state<T: Any>(self, v: State<T>) -> Self {
self.container.borrow_mut().bind(v);
self
}
@ -247,3 +261,25 @@ impl Terminal {
crossterm::terminal::size().unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "sync")]
#[test]
fn test_threaded_state() {
use crate::prelude::{App, State, ViewContext};
#[derive(Default)]
struct S {
i: i32,
}
let root_view = |_: &mut ViewContext| {};
let state = State::new(S::default());
App::new(root_view).bind_state(state.clone());
std::thread::spawn(move || {
state.get_mut().i = 10;
});
}
}

View File

@ -1,9 +1,13 @@
#[cfg(not(feature = "sync"))]
use std::{cell::RefCell, rc::Rc};
#[cfg(feature = "sync")]
use std::sync::{Arc, RwLock};
use std::{
any::{Any, TypeId},
cell::RefCell,
collections::HashMap,
ops::Deref,
rc::Rc,
};
use crate::context::ViewContext;
@ -36,10 +40,19 @@ impl Container {
/// A wrapper for state objcets. This internally holds a reference counted
/// poitner to the object and is used when injecting itno functions.
#[cfg(not(feature = "sync"))]
pub struct State<T: ?Sized>(Rc<RefCell<T>>);
#[cfg(feature = "sync")]
pub struct State<T: ?Sized>(Arc<RwLock<T>>);
impl<T> State<T> {
/// Create a new state wrapper.
#[cfg(feature = "sync")]
pub fn new(val: T) -> Self {
State(Arc::new(RwLock::new(val)))
}
#[cfg(not(feature = "sync"))]
pub fn new(val: T) -> Self {
State(Rc::new(RefCell::new(val)))
}
@ -55,6 +68,11 @@ impl<T> State<T> {
/// state.get_mut().0 = 6;
/// assert_eq!(state.get().0, 6);
/// ```
#[cfg(feature = "sync")]
pub fn get_mut(&self) -> std::sync::RwLockWriteGuard<T> {
self.0.write().unwrap()
}
#[cfg(not(feature = "sync"))]
pub fn get_mut(&self) -> std::cell::RefMut<T> {
RefCell::borrow_mut(&self.0)
}
@ -68,6 +86,11 @@ impl<T> State<T> {
/// let state = State::new(MyState(4));
/// assert_eq!(state.get().0, 4);
/// ```
#[cfg(feature = "sync")]
pub fn get(&self) -> std::sync::RwLockReadGuard<T> {
self.0.read().unwrap()
}
#[cfg(not(feature = "sync"))]
pub fn get(&self) -> std::cell::Ref<T> {
RefCell::borrow(&self.0)
}
@ -88,10 +111,20 @@ impl<T: ?Sized + 'static> FromContainer for State<T> {
/// A wrapper for resources stored within the app. This wrapper is returned
/// when objects are injected into component functions and provide immutable
/// access
#[cfg(feature = "sync")]
#[derive(Debug)]
pub struct Res<T: ?Sized>(Arc<T>);
#[cfg(not(feature = "sync"))]
#[derive(Debug)]
pub struct Res<T: ?Sized>(Rc<T>);
impl<T> Res<T> {
#[cfg(feature = "sync")]
pub fn new(val: T) -> Self {
Res(Arc::new(val))
}
#[cfg(not(feature = "sync"))]
pub fn new(val: T) -> Self {
Res(Rc::new(val))
}
@ -109,6 +142,16 @@ impl<T: ?Sized> Clone for Res<T> {
}
}
#[cfg(feature = "sync")]
impl<T: ?Sized> Deref for Res<T> {
type Target = Arc<T>;
fn deref(&self) -> &Arc<T> {
&self.0
}
}
#[cfg(not(feature = "sync"))]
impl<T: ?Sized> Deref for Res<T> {
type Target = Rc<T>;