Lets take vector for example. Vector in Rust is equivalent to
struct Vec {
ptr: NonNull<u8>,
allocator: Allocator,
capacity: usize,
len: usize
}
Then to create it and reference to it is
let vec = Vec::with_capacity(1024)
let reference = &vec
The variable vec contains metadata/the struct above that is saved in stack. The pointer inside the metadata / the first field of the struct above points to a heap memory. The reference contains memory address to vector's stack metadata, not to the heap directly. As a result, the heap can realloc without invalidating the reference because the reference only point to the stack metadata. But as another result, when the reference is shared to other thread, then the current function finish firdt before said thread, the vector's stack metadata will be dropped because stack memory that is dropped after a function exit. Thus, it makes the thread holding a zombie memory addrees aka invalid memory address. Thus it is use after free bug
Box is a way to move the stack metadata above to heap, so it will not be dropped by stack deallocation. But Box also has deallocation. Every heap deallocator is called at the end of the scope, unless it is leaked explicitely with Box::leak or std::mem::forget. So it stills will be dropped at the end of the scope. Comeback to the previous use after free problem
Now Rc is like Box, it moves the stack metadata to heap, then it adds integer counter. Everytime new reference is obtained via clone, it adds the counter. Everytime the reference is dropped, it decrease the counter. It removes the drop at the end of the scope to drop when counter = 0. Because the metadata now in heap and undropped by end of scope, the current function can exit safely without making removing the data. As a result you carry it around to cross function
Now Arc is like Rc, it moves the stack metadata to heap, then it changes from integer counter to atomic counter. If there is new reference that is registered to point to it using clone, the counter increase. If the is registered reference to it that is dropped, the counter decrease. If the counter goes to 0, the memory the Arc hold, the metadata and the heap buffer are dropped, in order to make it works it deactivate the normal dropping memory after the end of the scope, it is moved to dropping memory after the counter touch 0. When the new reference obtained from Arc using clone is created, the counter becomes +1, then said reference is sent to other thread. As a resultz the current function can exit safely without dropping the metadata that is still being pointed by the other thread, because now the metadata is saved in heap and not dropped at the end of the scope. After the other thread is done using the reference, it drops the reference that makes the counter become -1. 0 + 1 - 1 = 0, the counter hits 0, giving signal to Arc that now none point to this data, thus it is safe to drop this data, then the Arc drops the data from heap
Which one is faster?
Pure reference and pointer, &, &mut, *const, *mut
Why Box, Rc, and Arc are slower?
- It does heap allocation to save the metadata, heap allocation is incredibly slower than atomic. It happens 1 time at the creation, so avoid creating them inside a loop if possible
- It needs integer counter, but this one is cheap but still additional overhead compared to no counter at all
Edit: I just reread the discussion and realized I misstyped Rc and Arc :< how to think the root problem and the flow is still same :
All heap data structure has metadata saved in stack -> every reference to it only point to this stack metadata's address not to the heap buffer directly otherwise the heap buffer can not dynamically grow safely -> thus makes when the stack exit, they point to invalid address -> comes Box that moves the metadata to heap -> Box still has drop functioning at the end of scope -> comes Rc and Arc that also move the metadata to heap and adds counter as a toggle of when to drop the data