Hello. I want to avoid passing around an AtomicUsize to multiple functions and instead be able to access it from any thread by using it inside an RwLock. Is it both correct and efficient to read the AtomicUsize from the RwLock without blocking, and modify it afterwards?
use tokio::sync::{RwLock};
use lazy_static::lazy_static;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
pub struct Counters {
pub success: AtomicUsize,
}
pub struct ResourceManager {
pub counter: Arc<Counters>,
}
lazy_static! {
pub static ref RESOURCE_MANAGER: RwLock<ResourceManager> = RwLock::new(ResourceManager {
counter: Arc::new(Counters {
success: AtomicUsize::new(0),
}),
});
}
async fn inc() {
for i in 0..100000 {
let resource_manager = RESOURCE_MANAGER.read().await;
resource_manager.counter.success.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::main]
async fn main() {
let handle = tokio::spawn(inc());
let handle2 = tokio::spawn(inc());
handle2.await.unwrap();
handle.await.unwrap();
let resource_manager = RESOURCE_MANAGER.read().await;
println!("{:?}", resource_manager.counter.success.load(Ordering::SeqCst));
}
In this particular example the RwLock is not needed since you never actually mutate through it, so you can just remove it Rust Playground
Moreover, you're using an Arc but you're never cloning it. The sharing between tasks actually comes from the fact that you're using a static. Thus you can remove the Arc too Rust Playground
Finally, after removing the Arc::new call from the static the expression becomes completly const and thus doesn't need lazy_staticRust Playground
ps: I suggest you to use once_cell::sync::Lazy or std::cell::OnceCell (when that gets stabilized on stable) instead of the old lazy_static.
I'm going to mutate it later, of course. It's just an example. As for Arc - I'm going to clone it as well. The example above is just to show how I'd like to change an atomic value.
Yes. For example, I have a new member which is initialized at runtime using RwLock::write().
Maybe I won't need it if I have access to RESOURCE_MANAGER from everywhere, so I fixed it.
Yes, I have third-party Counters that contains atomics. The question is whether I can access the object through the resource manager without blocking it. Otherwise, it would become inefficient.
use tokio::sync::{RwLock};
use lazy_static::lazy_static;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Default)]
pub struct Config {
pub port: u16,
}
pub struct Counters {
pub success: AtomicUsize,
}
pub struct ResourceManager {
pub counter: Counters,
pub config: Config,
}
lazy_static! {
pub static ref RESOURCE_MANAGER: RwLock<ResourceManager> = RwLock::new(ResourceManager {
counter: Counters {
success: AtomicUsize::new(0),
},
config: Config::default(),
});
}
async fn inc() {
for _ in 0..10 {
let resource_manager = RESOURCE_MANAGER.read().await;
resource_manager.counter.success.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::main]
async fn main() {
{
// get port at runtime
let mut resource_manager = RESOURCE_MANAGER.write().await;
resource_manager.config.port = 1010;
}
let handle = tokio::spawn(inc());
let handle2 = tokio::spawn(inc());
handle2.await.unwrap();
handle.await.unwrap();
let resource_manager = RESOURCE_MANAGER.read().await;
println!("{:?}", resource_manager.counter.success.load(Ordering::SeqCst));
}
Atomics are already Send + Sync, so they can be shared across threads with plain old &T references. They do not need Arc, and definitely don't need RwLock especially if one of the design constraints is to update the counters without blocking.
This gives you global access to ResourceManager, which contains atomics that are not behind a lock. Just increment them without blocking:
// NOTE: No longer needs to be async. No longer blocks on a lock
fn inc() {
for _ in 0..10 {
RESOURCE_MANAGER.counter.success.fetch_add(1, Ordering::SeqCst);
}
}
And you might be ok with Ordering::Relaxed in this use case.