Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a load rather than a fence when dropping the contents of an Arc. #41714

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions src/liballoc/arc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,11 +751,11 @@ unsafe impl<#[may_dangle] T: ?Sized> Drop for Arc<T> {
return;
}

// This fence is needed to prevent reordering of use of the data and
// This load is needed to prevent reordering of use of the data and
// deletion of the data. Because it is marked `Release`, the decreasing
// of the reference count synchronizes with this `Acquire` fence. This
// of the reference count synchronizes with this `Acquire` load. This
// means that use of the data happens before decreasing the reference
// count, which happens before this fence, which happens before the
// count, which happens before this load, which happens before the
// deletion of the data.
//
// As explained in the [Boost documentation][1],
Expand All @@ -767,8 +767,39 @@ unsafe impl<#[may_dangle] T: ?Sized> Drop for Arc<T> {
// > through this reference must obviously happened before), and an
// > "acquire" operation before deleting the object.
//
// Note that Rust is not C++, and so it's valid to ask whether we could
// do without the Acquire/Release handshake here. The contents of an Arc
// are immutable, and thus we could hypothetically assume that there are
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contents aren't immutable, due to the potential for interior mutability (through UnsafeCell or the atomics you mention below). When thinking about this fence in the past, I'd always considered it as ensuring that any outstanding interior non-Relaxed writes are visible prior to the destructor running.

I do agree that allocators in general are virtually guaranteed to have enough fencing to at least avoid relaxed writes landing after the memory is transferred to another thread. But I worry about people relying on ordering with respect to the destructor code running.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When thinking about this fence in the past, I'd always considered it as ensuring that
any outstanding interior non-Relaxed writes are visible prior to the destructor running.

Oh I see. I'd been thinking of such writes as being either atomic (in which case they're governed by their own memory ordering) or protected by something like a Mutex, which have their own synchronization. But I realize now that the destructor doesn't actually acquire the mutex before dropping the contents, and so we can't rely on its internal synchronization to flush the writes.

// no writes to be flushed with the Release and thus there's no need to
// Acquire to pick them up. In that case, we could just use a single
// Relaxed operation here, and improve performance on ARM.
//
// However, things get a bit sketchy in the case of Arc<AtomicFoo>.
// Suppose that there are three threads (A, B, and C). Threads A and B
// each hold a strong reference to the arc. Thread A does a relaxed store
// to the atomic, and then drops the Arc, dropping the strong count to 1.
// Thread B then drops the arc, notices the count is now zero, and
// deallocates the ArcInner<AtomicFoo>.
//
// In this case, thread B is not guaranteed to observe the store to the
// AtomicFoo before handing the memory off to the allocator. This is ok,
// but if thread C were to recycle that memory without observing the
// store, then its newly-allocated memory would get clobbered at some
// later point, which would be catastrophic.
//
// Whether or not thread C is guaranteed to observe the store before
// the memory is allocated depends on what kind of memory ordering the
// allocator uses to synchronize the recyling of memory between threads.
// It needs _something_ to flush writes from thread B before the memory
// is transferred to thread C. But that could be accomplished by Release/
// Acquire operations by threads B and C on a shared atomic, which
// doesn't give us any guarantee about Relaxed writes from thread C.
//
// In practice, most allocators probably do conservative enough fencing
// that this is a non-issue. But we play it safe.
//
// [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html)
atomic::fence(Acquire);
self.inner().strong.load(Acquire);

unsafe {
self.drop_slow();
Expand Down