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

Weak pointers #14

Merged
merged 17 commits into from May 11, 2017

Conversation

@vitvakatu
Copy link
Collaborator

commented May 8, 2017

Weak pointers are used to avoid memory leaking when trying to drop structures with cycled linking (e.g. Base has pointers to Childs and every Child has pointer to `Base)

Implementation is very rough atm, there're some places where we need to optimize performance or to provide better API.
I'm looking for your feedback.

Fixes #9

@kvark kvark self-requested a review May 8, 2017

@vitvakatu vitvakatu force-pushed the vitvakatu:weak-pointers branch from 2c33185 to 1abf891 May 8, 2017

@kvark
Copy link
Owner

left a comment

Unfortunately, I don't think this is going to work just yet.

Supposing you have a weak pointer. It's counter is in meta[id].weak. The item loses the last strong pointer, gets added to the free list, then gets re-used. It's weak counter is reset then. Will you be able to access the newly created element by an old weak pointer?

Here are 2 suggestions to make it great:

  1. have an epoch associated with any weak pointer and stored in the meta. This way you'll be able to detect when an old weak pointer is trying to access the newer element of the storage.
  2. hide the whole thing behind a feature. Since it's expanding a meta item size from 1 word to 3, the cache utilization will suffer, and we don't want most of the users to pay for that.

struct Base {
value: i32,
childs: Vec<Pointer<Child>>,

This comment has been minimized.

Copy link
@kvark

kvark May 8, 2017

Owner

nit: children

};
println!("{} has parent with value {}", child.value, parent_value);
}
}

This comment has been minimized.

Copy link
@kvark

kvark May 8, 2017

Owner

ni: newline missing


base.childs.push(child1.clone());
base.childs.push(child2.clone());
let base_storage = Storage::new();

This comment has been minimized.

Copy link
@kvark

kvark May 8, 2017

Owner

it's great to have cross-storage support, but maybe we should make the example really simple and use a single storage instead? just for clarity - people will use it for learning purposes, not just feature demonstration

src/lib.rs Outdated
for index in pending.sub_ref_strong.drain(..) {
s.meta[index].strong -= 1;
if s.meta[index].strong == 0 {
s.meta[index].weak -= 1;

This comment has been minimized.

Copy link
@kvark

kvark May 8, 2017

Owner

where does this come from?

src/lib.rs Outdated
sub_ref: Vec::new(),
add_ref_strong: Vec::new(),
sub_ref_strong: Vec::new(),
add_ref_weak: Vec::new(),

This comment has been minimized.

Copy link
@kvark

kvark May 8, 2017

Owner

I wonder why do we even need to store the weak counters? They aren't holding anything alive anyway, so we might as well not bother.

src/lib.rs Outdated
sub_ref: Vec<usize>,
add_ref_strong: Vec<usize>,
sub_ref_strong: Vec<usize>,
add_ref_weak: Vec<usize>,

This comment has been minimized.

Copy link
@kvark

kvark May 8, 2017

Owner

Do we actually need to store the weak pointer count in the meta? It's not obvious to me, why we'd do that.

@vitvakatu vitvakatu force-pushed the vitvakatu:weak-pointers branch from 1abf891 to 853a4b5 May 8, 2017

@vitvakatu

This comment has been minimized.

Copy link
Collaborator Author

commented May 8, 2017

Well, it was a big mistake. I misunderstood the implementation of Weak from standard library.
Now I rewrote the whole stuff with attention to your notices.
It's not ready yet, need to add epoch or similar mechanic, as you noticed.

@kvark
Copy link
Owner

left a comment

I think we are almost there. A few more notes.

src/lib.rs Outdated
impl<T> Pointer<T> {
/// Creates a new `WeakPointer` to this component.
pub fn downgrade(&self) -> WeakPointer<T> {
let epoch = self.target.read().unwrap().epoch[self.index];

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

I'd like to never ever lock the target of a pointer inside the pointer methods. Imagine locking a storage for writing, then trying to downgrade one of its pointers. BAM, deadlock!
Instead, every strong pointer should carry the epoch internally, passed at creation, since the one it target.epoch is guaranteed to be the same for the lifetime of any such pointer.

src/lib.rs Outdated
match self.target.upgrade().and(self.pending.upgrade()) {
None => None,
Some(arc) => {
let target = self.target.upgrade().unwrap();

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

calling self.target.upgrade() twice - let's just bail out early:

let target = match self.target.upgrade() {
   Some(target) => target,
   None => return None,
};

side note: I wonder if/when the ? syntax would start working with options, it would help here

src/lib.rs Outdated
None => None,
Some(arc) => {
let target = self.target.upgrade().unwrap();
if target.read().unwrap().meta[self.index] == 0 ||

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

Again, I think locking the target here is just asking for a deadlock.
Perhaps, we could have upgrade as a method of Storage instead? (or ReadLock/WriteLock - needs investigation)

This comment has been minimized.

Copy link
@vitvakatu

vitvakatu May 9, 2017

Author Collaborator

Well, interesting proposal.
I think upgrade as Storage method makes no sense - you still need lock.
As the most often use case will be accessing Pointer soon after upgradeing it, we can add new methods for Locks: access_weak and access_weak_mut which will return Result<Pointer<T>>. But we still need regular upgrade method.

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

If possible, I'd like the pointer to not even be able to lock the storage. The fact it's currently possible is purely accidental.

access_weak and access_weak_mut which will return Result<Pointer>.
that would be inconsistent with our current access semantics

But we still need regular upgrade method.

Do we? That use-case is not obvious to me.

This comment has been minimized.

Copy link
@vitvakatu

vitvakatu May 9, 2017

Author Collaborator

Oh, I made a mistake. Not Result<Pointer<T>>, but Result<&T> and Result<&mut T>.
As for upgrade method - I'm not sure it's useful, but in some situations user can need store Pointer produced by Weak

src/lib.rs Outdated
let target = self.target.upgrade().unwrap();
if target.read().unwrap().meta[self.index] == 0 ||
target.read().unwrap().epoch[self.index] != self.epoch {
return None;

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

I think we can introduce richer semantic here at no cost. Make it return a Result, where the error may be either the storage itself is dead, or the element is dead, or the epoch is wrong.

src/lib.rs Outdated
impl<T> PartialEq for WeakPointer<T> {
fn eq(&self, other: &WeakPointer<T>) -> bool {
match self.upgrade().and(other.upgrade()) {
Some(other_arc) => self.upgrade().unwrap() == other_arc,

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

similarly, this is calling self.upgrade twice

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

do we even need to upgrade the pointers for comparison?..

This comment has been minimized.

Copy link
@vitvakatu

vitvakatu May 9, 2017

Author Collaborator

Do we even need to compare pointers? :)

This comment has been minimized.

Copy link
@kvark

kvark May 9, 2017

Owner

It's not critical but could be useful, I think

@vitvakatu vitvakatu force-pushed the vitvakatu:weak-pointers branch from d3b6dac to 225b49f May 10, 2017

@vitvakatu vitvakatu force-pushed the vitvakatu:weak-pointers branch from 225b49f to cbc2238 May 10, 2017

src/lib.rs Outdated
target: self.storage.clone(),
pending: self.pending.clone(),
}
}

/// Upgrades the `WeakPointer` to an `Pointer`, if possible.

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

"an" -> "a"

src/lib.rs Outdated
/// TODO: control by a cargo feature
type Epoch = u16;

pub enum UpgradeErr {

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

that must be issuing a warning for missing docs

src/lib.rs Outdated
/// Shared pointer to the pending updates.
type PendingRef = Arc<Mutex<Pending>>;
type WeakPendingRef = Weak<Mutex<Pending>>;

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

this, as well as WeakStorageRef, are only used once. Perhaps, these typedefs are not really useful here

src/lib.rs Outdated
index: usize,
epoch: Epoch,
target: WeakStorageRef<T>,
pending: WeakPendingRef,

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

I don't think we need to fiddle with the weak pending reference, let's just use PendingRef here

src/lib.rs Outdated
target: self.storage.clone(),
pending: self.pending.clone(),
}
}

/// Upgrades the `WeakPointer` to an `Pointer`, if possible.

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

"an" -> "a"

src/lib.rs Outdated
target: self.storage.clone(),
pending: self.pending.clone(),
}
}

/// Upgrades the `WeakPointer` to an `Pointer`, if possible.
/// Returns `Err` if the strong count has reached zero and the inner value was destroyed.

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

"and" -> "or"

src/lib.rs Outdated

/// Upgrades the `WeakPointer` to an `Pointer`, if possible.
/// Returns `Err` if the strong count has reached zero and the inner value was destroyed.
pub fn upgrade(&self, ptr: &WeakPointer<T>) -> Result<Pointer<T>, UpgradeErr> {

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

we'll need to seriously make this thing nice
first off, when epoch is bumped on deletion, this code no longer needs to check for the refcount == 0
secondly, now that we don't need guard, we don't actually need &self to be ReadLock/WriteLock either!
so we can make upgrade just a method of WeakPointer, as you initially wanted, but without the danger of dead-locking the Storage.

src/lib.rs Outdated
if pending.epoch[ptr.index] != ptr.epoch {
return Err(UpgradeErr::WrongEpoch);
}
else {

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

else is not needed

src/lib.rs Outdated
}
else {
pending.add_ref.push(ptr.index);
return Ok(Pointer {

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

return is not needed

@kvark
Copy link
Owner

left a comment

One more step!

src/lib.rs Outdated
}

impl Pending {
fn prepare_sub_drain(&mut self) -> (&mut Vec<usize>, &Vec<Epoch>) {

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

let's return (Drain<usize>, &[Epoch]) pair here and rename the method to drain_sub

src/lib.rs Outdated
@@ -213,6 +289,14 @@ impl<'a, T> ReadLock<'a, T> {
&self.guard.data[ptr.index]
}

/// Borrow a weak pointed component for reading.
pub fn access_weak(&self, pointer: &WeakPointer<T>) -> Result<&T, UpgradeErr> {

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

I don't think access_weak brings anything on the table, really. It may seem useful, but you can do the same from user space and that would be as efficient, so I'd be in favour of reducing the API surface here

src/lib.rs Outdated
s.meta[index] -= 1;
if s.meta[index] == 0 {
s.free_list.push(index);
s.free_list.push((index, epoch[index] + 1));

This comment has been minimized.

Copy link
@kvark

kvark May 10, 2017

Owner

I think we actually need to bump the epoch here, as in epoch[index] += 1, unless I'm missing the place where you are already doing that.

@kvark

kvark approved these changes May 11, 2017

storage.read()
.access_weak(next)
.ok().map_or("None".into(), |item| item.value.clone())
let value = node.next.as_ref().map_or("None".into(), |ref next| {

This comment has been minimized.

Copy link
@kvark

kvark May 11, 2017

Owner

this seems quite complicated, I'm going to think on reducing it, but it's not essential to the PR

src/lib.rs Outdated
@@ -55,8 +55,8 @@ struct Pending {
}

impl Pending {
fn prepare_sub_drain(&mut self) -> (&mut Vec<usize>, &Vec<Epoch>) {
(&mut self.sub_ref, &self.epoch)
fn drain_sub(&mut self) -> (std::vec::Drain<usize>, &mut [Epoch]) {

This comment has been minimized.

Copy link
@kvark

kvark May 11, 2017

Owner

niiiice. Just one nit: make the use of Drain consistent with the rest of the code, so have use std::vec::Drain at the top (right after use std::sync... line).


for node in storage.read().iter() {
let value = node.next.as_ref().map_or("None".into(), |ref next| {
next.upgrade().ok().map_or("None".into(), |ref ptr| {

This comment has been minimized.

Copy link
@kvark

kvark May 11, 2017

Owner

can't we just unwrap here, considering that we know the storage and target elements are alive?

This comment has been minimized.

Copy link
@vitvakatu

vitvakatu May 11, 2017

Author Collaborator

We can, of course. I'll do it to simplify example, but I think we need to reduce it somehow.

@kvark kvark changed the title [WIP] Weak pointers Weak pointers May 11, 2017

@kvark kvark merged commit 64fa5b2 into kvark:master May 11, 2017

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
@kvark

This comment has been minimized.

Copy link
Owner

commented May 11, 2017

IT HAPPENED! 🍰 🍰 🍰

@vitvakatu vitvakatu deleted the vitvakatu:weak-pointers branch May 11, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.