Skip to content

Drop not called if main thread exits before child thread is done #4

Closed

Description

@cfsamson

Thanks for sharing this. I actually tried something like this once but got stuck on a problem, so I thought I'd check if you found a solution to it, but it seems to still cause issues:

use defer_drop;
use std::thread;

struct ToDrop(String);

impl Drop for ToDrop {
    fn drop(&mut self) {
       println!("Hello!");
    }
}

fn main() {
    for i in 1..=1000 {
        test(i);
    }

}

pub fn test(n: usize) {
    let s = format!("Test {}", n);
    let todrop = ToDrop(s);
    let _guard = defer_drop::DeferDrop::new(todrop);
}

This panics (only tested on Windows) with the message:

thread '<unnamed>' panicked at 'cannot access stdout during shutdown', src\libstd\io\stdio.rs:527:25

Granted, it's a bit contrived to write to stdout on drop but that's only a symptom of the problem which is that drop is not guaranteed to be called if the main thread finished before the child thread doing the actual dropping.

I've tried to find any documentation about the guarantees on Drop but haven't found anything saying that drop must be called, the closest thing I could find was this discussion in the nomicon repo. This seems to imply that there is some kind of guarantee that safe code might rely on.

I've tried to save the thread handle for the child thread and join it when the GARBAGE_CAN is dropped, but statics seems to be dropped so late that it at least causes problems when relying on the drop of a static to join a child thread (on Windows at least).

I'm not 100 % sure this is a serious problem or not but I thought I'd at least make you aware of it.

Activity

  1. changed the title [-]Panics if main thread exits before child thread is done[/-] [+]Drop not called if main thread exits before child thread is done[/+] on May 22, 2020
  2. Lucretiel commented on May 23, 2020

    @Lucretiel
    Owner

    Thanks for the report! This is, in fact, intended (or, at least, expected) behavior; we don't provide any guarantees about when or if the destructors run. There are a few reasons for this:

    • There's no way to automatically block the main thread when it ends.
    • Hypothetically we could provide a wait_for_deferred_drops global function, but that function could block indefinitely, and in any case there's no way for it to be correct in the technical sense, because it might return successfully immediately a new item is added to the defer queue.

    This behavior is called out in the Notes section of the documentation:

    Additionally, there are no guarantees about how long the values will be queued before being dropped, or even that they will be dropped at all. If your main thread terminates before all drops could be completed, they will be silently lost (as though via a mem::forget). This behavior is entirely up to your OS's thread scheduler.

    In general, because the behavior of this library is inherently non-deterministic (because it's completely reliant on your OS's thread scheduler), I'm content to continue to require users to expect that the drop may be deferred for any amount of time, including forever.


    I've tried to find any documentation about the guarantees on Drop but haven't found anything saying that drop must be called, the closest thing I could find was this discussion in the nomicon repo. This seems to imply that there is some kind of guarantee that safe code might rely on.

    I have a lot of very contentious opinions about this, but the current consensus is that you cannot rely in-general on destructors being run to enforce the safety invariants of your code. In particular, the safe function std::mem::forget forgets an object without running its drop method. The rationalle for this is discussed in the Leaking section of the nomicon.

    I've tried to save the thread handle for the child thread and join it when the GARBAGE_CAN is dropped, but statics seems to be dropped so late that it at least causes problems when relying on the drop of a static to join a child thread (on Windows at least).

    As it happens, statics are never dropped. This is by design. The 'static lifetime should be considered "forever", not just "until main ends". Formally, the 'static lifetime is "longer than all other lifetimes", and the only way for that to be possible is if 'static objects are never dropped. [source]

  3. cfsamson commented on May 23, 2020

    @cfsamson
    Author

    Thanks for the prompt and thorough reply.

    In general, because the behavior of this library is inherently non-deterministic (because it's completely reliant on your OS's thread scheduler), I'm content to continue to require users to expect that the drop may be deferred for any amount of time, including forever.

    Yeah, this is fine and pretty clearly written in the documentation. What got me uncertain was this comment in the thread I linked to, but if you're at the point where you've got contentious opinions on this subject you've certainly given this much more thought than I have :)

    As it happens, statics are never dropped.

    Thanks. I learned something new!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions