How can I determine if I have a unique Arc when it's dropped?

Viewed 572

I've an Arc<Mutex<Thing>> field in a struct which is cloned many times. It is shared between concurrent threads. Drop::drop is called for each clone as it goes out of scope. Is there any way to determine when Drop::drop is called for the last (unique) Arc<Mutex<Thing>>?

  • It's clear that strong_count is subject to data races (I've seen them). So, you can't count on Arc::strong_count() == 1 (no pun intended).

  • I found that I couldn't use Arc::try_unwrap() due to a move issue.

  • Arc::is_unique() is private.

Other than keeping a Arc<AtomicUsize> field, which is incremented on clone and decremented on drop, is there any way to determine if a drop is for a unique Arc<Mutex<Thing>>?

Here's an MRE:

use std::sync::{Arc};

#[derive(Debug)]
enum Action {
    One, Two, Three
}

// Thing trait which operates on an Action, which should be a enum, allowing for
// different action sets.
trait Thing<T> {
    fn disconnected(&self);
    fn action(&self, action: T);
}

// There are many instances of an ActionController.
// There may be zero or more clones of an instance.
// The final drop of the instances should call thing.disconnected()
// In a multi-core environment, the same instance may be running on multiple cores
// ActionController should not be generic.
#[derive(Clone)]
struct ActionController {
    id: usize,
    thing: Arc<dyn Thing<Action>>,
}
impl ActionController {
    fn new(id: usize, thing: Box<dyn Thing<Action>>) -> Self {
        Self { id, thing: Arc::from(thing) }
    }
    fn invoke(&self, action: Action) {
        self.thing.action(action);
    }
}

//
// To work around the drop issue, I've implemented Clone for ActionController which
// performs a fetch_add(1) on clone and a fetch_sub(1) on drop. This provides
// suficient information to call disconnected() -- but it just seems like there's
// got to be a better way.
impl Drop for ActionController {
    fn drop(&mut self) {
        // drop will be called for each clone of an Controller instance. When
        // the unique instance is dropped, disconnected() must be called
        self.thing.disconnected();
    }
}

struct Controlled {}
impl Thing<Action> for Controlled {
    fn disconnected(&self) { println!("disconnected")}
    fn action(&self, action: Action) {println!("action: {:#?}", action)}
}

fn bad() {
    let controlled = Controlled{};
    let controlled = Box::new(controlled) as Box<dyn Thing<Action>>;
    let controller = ActionController::new(1, controlled);
    let clone = controller.clone();
    controller.invoke(Action::One);
    clone.invoke(Action::Two);
    drop (controller);
    clone.invoke(Action::Three);
}

fn main() {
    bad();
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn incorrect() {
        bad();
    }
}
4 Answers

Arc::try_unwrap is probably the intended way to do this - is it possible to restructure your code to avoid the move issues you were running into?

Why do you want to know? If you have some extra cleanup code that needs to be executed before the Mutex<Thing> is dropped, maybe you could use an Arc<MyLockedThing> instead, where MyLockedThing is a struct containing a Mutex<Thing> that impls Drop to do the cleanup?

It seems like you want to be notified when the data inside the Arc is to be dropped. If so, this can be done by implementing Drop on the type "inside" the Arc.

Define a newtype:

struct ThingAction(Box<dyn Thing<Action>>);

impl Thing<Action> for ThingAction {
    fn disconnected(&self) {
        self.0.disconnected()
    }
    
    fn action(&self, action: Action) {
        self.0.action(action)
    }
}

And implement Drop:

impl Drop for ThingAction {
    fn drop(&mut self) {
        self.disconnected()
    }
}

Then use the newtype:

#[derive(Clone)]
struct ActionController {
    id: usize,
    thing: Arc<ThingAction>,
}
impl ActionController {
    fn new(id: usize, thing: Box<dyn Thing<Action>>) -> Self {
        Self { id, thing: Arc::new(ThingAction(thing)) }
    }

I don't think there's any perfect way to do this without stdlib support (go checkout out Arc::drop).

Weak::strong_count or Weak::upgrade is less subject to races so if you downgrade your Arc then drop it, if the weakref's strong count is 0 or trying to upgrade it fails you know the Arc is dead, but there is no guarantee the current thread killed it, two might have concurrently dropped the Arc at the same time before either had the time to check for the weakref's strong count.

I think the only bulletproof way would be to get notified by a Drop stored inside the Arc, that you're guaranteed is only called once.

Related