Drop not called if main thread exits before child thread is done #4
Description
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
[-]Panics if main thread exits before child thread is done[/-][+]Drop not called if main thread exits before child thread is done[/+]Lucretiel commentedon May 23, 2020
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:
wait_for_deferred_dropsglobal 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
Notessection of the documentation: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 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::forgetforgets an object without running its drop method. The rationalle for this is discussed in theLeakingsection of the nomicon.As it happens, statics are never dropped. This is by design. The
'staticlifetime should be considered "forever", not just "until main ends". Formally, the'staticlifetime is "longer than all other lifetimes", and the only way for that to be possible is if'staticobjects are never dropped. [source]cfsamson commentedon May 23, 2020
Thanks for the prompt and thorough reply.
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 :)
Thanks. I learned something new!