Does inserting into a `HashMap` invalidate references in a multithreaded context?

I there!
I have the base struct for my library:

pub struct BlackBox<U: ?Sized> {
    data: HashMap<TypeId, Box<U>>,

And this is meant to be multithreaded if the supplied U is the multithreaded versions of certain traits in my library. In theory, the only things that can be done with this HashMap are the following:

  • Read an immutable reference given a TypeId (My library uses quite a bit of interior mutability)
  • Add another entry with the following method:
pub fn allocate_for<T: 'static + Send>(&mut self) {
        .or_insert_with(|| Box::new(RwLockUnit::new(StorageUnit::<T>::new())));

Which is the only &mut function in the entire impl. Given this, would calling allocate_for::<T>() while using one of the read functions on a different thread, such as this:

fn unit_get<T: 'static + Send>(&self) -> DynamicResult<&U> {
        .map(|x| &**x)

lead to invalidating the data that is being used here? Or, for example, what if I hand out a RwLock[Read|Write]Guard to data inside the U?

In other words, is it safe for me to say the following:

unsafe impl Send
    for BlackBox<
        (RwLockTraitObject + Send),

unsafe impl Sync
    for BlackBox<
        (RwLockTraitObject + Send),

I think threading is actually irrelevant here. Your type should already auto-impl Send and Sync if U does. What makes you think you need the manual unsafe impl?

So I thought you might mean to ask about memory stability, if you were cheating lifetimes for borrowed values, but you’re not even doing that. So regardless of threading, you won’t be able to call allocate_for at all if any unit_get reference is still borrowed.

If you do cheat lifetimes with some unsafe code, the Box should be enough to provide stable memory addresses. Just make sure you tie the lifetimes somehow, and make sure you never remove/replace entries.

1 Like

You should already get the Send impl, but the Sync impl is definitely not safe because the types inside the trait object may not be Sync.

1 Like

Okay, so if I follow correctly:
It should auto-implement Sync as long as everything that I put in it (As in any T that I allocate_for) is Send + Sync. But the thing is that I would like it to look like this when being used:

fn main() {
    let mut storage = BlackBox<RwLockUnitTraitObject>::new();
    let storage = Arc::new(storage);
        let storage = storage.clone();
        thread::spawn(move || {

My RwLockUnit in the original post looks like this by the way:

pub struct RwLockUnit<T> {
    inner: RwLock<T>,
// Would usually look like this:
// RwLockUnit<StorageUnit<T>>
// Because `StorageUnit` impls functionality, and this is a wrapper.

And so, from this, and this explanation of send vs sync, I can deduct the following:

  • I can assure that this BlackBox is Send always, meaning that it can be moved into other threads because I can treat giving various threads unique locks to objects the same as Sending them around, and I also have a T: Send restrain on everything.
    But does this mean that I’m allowed to give multiple threads read locks on the same object?
  • I can assure that this BlackBox is Sync as long as U (RwLockUnit) is Sync

So I would need to use something like this:

BlackBox<RwLockUnitTraitObject + Send + Sync>

Note I cannot say Unit (trait which is actually RwLockUnitTraitObject) : Send + Sync because it is the same for RefCell, and the multithreaded versions, and changes given its associated types.

After some consideration, I’ve decided to do the following:
Wrap my BlackBox struct’s RwLock version in another type which would only allow T: Send + Sync + Any + 'static type storages to be allocated and implement Deref (Not DerefMut) so as to allow everything else to work as expected.

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