Using atomics inside RwLock

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_static Rust 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.

3 Likes

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.

Just to be sure:

  • you're going to mutate the Arc itself, not its contents (hence the RwLock around the Arc)
  • you're going to clone the Arc (hence the presence of Arc)
  • you want to mutate an usize that's associated with the contents of the Arc without a mutable reference

?

  1. Yes. For example, I have a new member which is initialized at runtime using RwLock::write().
  2. Maybe I won't need it if I have access to RESOURCE_MANAGER from everywhere, so I fixed it.
  3. 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));
}

If possible I would move the RwLock to be only around the Config or whatever you need to actually modify which don't already use atomics.

4 Likes

Would it be a mistake?

It's not a mistake, but it would introduce substantial overhead when mutating the atomic.

2 Likes

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.

2 Likes

I understand that. The idea was to avoid passing a lot of objects between threads and keep them together.

That doesn't have any effect whatsoever on how atomics can be used. It's just a design decision.

pub struct ResourceManager {
    pub counter: Counters,
    pub config: RwLock<Config>,
}

lazy_static! {
    pub static ref RESOURCE_MANAGER: ResourceManager = ...
}

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.

2 Likes

I'll try that. Thanks a lot!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.