Should I use an crossbeam::AtomicCell in this case?

Hi there!
I'm writing a library, which aims to serve as a dynamic storage solution. I want this to be able to be passed around threads, as long as the types contained are 'static + Send + Sync. I wanted to create two versions for multithreading, a Mutex version, and a RwLock version, as it currently uses a RefCell. Unfortunately both of these are missing a function similar to <Ref/RefMut>::map(). Writing a wrapper guard for this seems to be rather difficult to convince the compiler of due to lifetimes. So, I considered using another library, namely crossbeam, and its closest equivalent (I think) AtomicCell. From what I understand, this uses a kind of lock underneath, and atomic operations if it is operating on types that can be transmuted to supported atomic types.

So I have a few questions:

  • Should I even be using an external crate, if this could theoretically be written using std and just rust?
  • Is AtomicCell the right type to be using?
  • In the case I have a thread that has acquired a mutable reference to the data, and the type of the data is not transmutable, can I assume another thread will have to wait until the reference is dropped to be able to acquire the mutable reference again?
  • What if I have code like this:
struct Foo {
    a: usize,
    b: String,
    c: &'static str
}
struct Container(AtomicCell<Foo>);
impl Container {
    fn foo(&mut self) -> &mut usize {
        let x: &mut Foo = self.0.get_mut();
        &mut x.a //Would the lock be kept here?
    }
}
  • What about the fact that I want my interface to look like this:
trait Unit {
    fn get_mut(&self) -> Option<&mut Value>;
}
  • Would I then be required to own a RefCell<AtomicCell<T>>?
  • If I want my api to be not blocking, and instead just return an Option<&mut T>, can I check if it is currently locked?

Thanks for the help and sorry for the wall of text!

Rust encourages you to use external crates, even if you can write it with only std. So yes, you can use an external crate. In this case I would because writing a AtomicCell<T> is error prone and cross-beam already has a good version of it.

There is no lock on AtomicCell when using get_mut, because you are using &mut _ it is already guaranteed that you are accessing it without aliasing, so there is no lock in this case.
Moreover, AtomicCell<T> only uses locking internally when the target platform doesn't support atomics with the same size as T. But overall it can be thought of as using atomic operations all the time. AtomicCell works just like Cell, but with some added complexity to deal with multiple threads.

You can't have that api and still be useful, because there would be no way to track the &mut Value, and so the only safe implementation would give out a single &mut Value on its first call and no others and this implementation is useless. Instead you can ue RwLock or Mutex and return an Option<_> to their respective smart pointer guards (ex. Option<MutexGuard<...>>).

This doesn't make any sense, why would you compose two interior mutability types? This also gives up the Send/Sync of AtomicCell.

Only if you use smart pointers (like MutexGuard), not if you are using &mut T this is because &mut T doesn't track when it is last used, so if you are not careful, you can easily create multiple aliasing &mut T especially when multiple threads are involved.

1 Like

Ah, I see, so I want to return a smart pointer like how I did with RefCell. I'm sorry about the seemingly illogical questions, but some resources online led me to believe they were some sort of cpu-locking magic. :grimacing: On the other hand, are there any crates that you recommend to act as a multi-threaded RefCell, with the map() method mentioned above? The reason I need this, is because I need my reference to go like so:

RefCell<Container<T>> ->
Ref<'a, T> -> 
Ref<'a, dyn Any> ->
Ref<'a, T> 

So that my trait, Unit has no type attached, and can therefore be implemented like so:

struct Container<T>(T);
trait Unit {
    fn get(&self) -> &dyn Any;
}
impl<T> Unit for Container<T> {
    fn get(&self) -> &dyn Any {
        let x: &T = &self.0;
        &*x
    }
}
struct ManyUnits(HashMap<TypeId, Box<dyn Unit>>);

(With the interior mutability removed for simplicity)

Well, AtomicCell will use locking internally to guarantee that the operations are atomic, instead of using CPU instructions, but that is an implementation detail. If AtomicCell can use a CPU instruction it will.

Could you show how interior mutability comes into play?

is it something like this?

struct Container<T>(Mutex<T>);
trait Unit {
    fn get(&self) -> &dyn Any;
}
impl<T> Unit for Container<T> {
    fn get(&self) -> &dyn Any {
        ...
    }
}
struct ManyUnits(HashMap<TypeId, Box<dyn Unit>>);

parking_lot's MutexGuard and RwLock*Guard can be mapped, although it does return a different type (MappedMutexGuard<'_, T>). This is to get around a soundness issue.

When I searched for MutexGuard::map I found a proposal to add it to std, but it was rejected because it was unsound. Similarly for RwLock.

2 Likes

What about an idea like the following PoC?

#![deny(
    bare_trait_objects,
    elided_lifetimes_in_paths,
)]

mod lib {
    use ::std::{
        any::Any,
        ops::{
            Deref,
        },
        sync::{
            RwLock,
            RwLockReadGuard,
        },
    };

    // trait Alias
    pub
    trait Share : Send + Sync + 'static {}
    impl<T : Send + Sync + 'static> Share for T {}

    pub
    trait Unit<'a> : Share {
        /// Sync bounds are not needed, but the guarantee comes for free from Shared
        type Ret : Deref<Target = dyn Any + Sync> + Sync + 'a;

        fn get (self: &'a Self) -> Self::Ret;
    }

    pub
    struct Container<T : Sized + Share> /* = */ (
        RwLock<T>,
    );

    impl<T : Sized + Share> From<T> for Container<T> {
        fn from (value: T) -> Self
        {
            Container(RwLock::new(value))
        }
    }

    impl<'a, T : Sized + Share> Unit<'a> for Container<T> {
        type Ret = LazyAny<RwLockReadGuard<'a, T>>;

        fn get (self: &'a Self) -> Self::Ret
        {
            LazyAny(self.0.read().expect("Lock was poisoned"))
        }
    }

    pub
    struct LazyAny<Guard> (Guard)
    where
        Guard : Deref + Sync,
        Guard::Target : Sized + Sync + 'static,
    ;

    impl<Guard> Deref for LazyAny<Guard>
    where
        Guard : Deref + Sync,
        Guard::Target : Sized + Sync + 'static,
    {
        type Target = dyn Any + Sync;

        fn deref (self: &'_ Self) -> &'_ (dyn Any + Sync)
        {
            &*Guard::deref(&self.0)
        }
    }
}
use self::lib::*;

fn main ()
{
    use ::core::any::Any;

    let ft = Container::from(42);
    ::std::thread::spawn(move || {
        let ft_deref_any = ft.get();
        let v = vec![&*ft_deref_any as &dyn Any];
        for x in v {
            if let Some(&x) = x.downcast_ref() {
                let _: i32 = x;
                println!("x: i32 = {}", x);
            }
        }
    }).join().expect("Thread panicked");
}
1 Like

Yep! That's approximately what I have.

That sounds great! Now that I think about it, there is no reason to have an immutable version of the get and ind functions for the Mutex version, if the MutexGuard is already MutexGuard: DerefMut<Target = T> for any T.

That's so unfortunate! :sob:
@Yandros Thanks for the very good example! I think that working with the parking_lot version will be easier though, so I will probably use that. In the case I run into a roadblock with it, I will try your version instead

Thanks!

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.