7 comments

[ 3.3 ms ] story [ 28.4 ms ] thread
Cool thing, questioning the design though. Why do we need the Scope object? Why wouldn't a Thread object that joins when dropped work directly?
The problem is the Thread object can be forgotten such that it's never dropped. This would cause the thread to never be joined. This becomes a problem if the thread is borrowing anything from the stack since the borrow could then outlive the stack frame.
What happens if you borrow a more complex mutable object to a thread and this thread is killed somehow leaving the borrowed object in a corrupt state, e.g because the thread was killed while doing an unsafe operation on the borrowed object. Can't I catch_unwind the scope call and then access the corrupted object?
Unwind safety is an underdeveloped aspect of rust. Unwinding is not meant to be used for control flow. catch_unwind should be used to protect FFI callbacks and restart unwinding on the other side. I would consider its use in thread pools to be an abuse of the feature.

That said, given that unwinding does exist, it is the responsibility of unsafe code blocks to account for it. They must not allow safe code to create UB. That they mostly don't account for it is a culture issue.

This was the original implementation in the standard library way back. The language evolved in such a way that made it unsound.

It is a pretty fundamental feature of the current version of rust (search term: NLL) that borrows do not end upon the last reference (or any type with lifetime parameter, but I'll just say "reference") with that lifetime being dropped; rather, they end when the last reference will no longer be used. This is crucial to ergonomics; otherwise you could not have:

    let x = vec![];
    let y = &mut x;
    y.push(0);
    x.push(1);
because, strictly speaking, y is dropped at the end of the block.

Dropping a reference quite often doesn't even count as a use (#[may_dangle] types, and actual references). But even if it does, if the drop for any reason doesn't happen, then it's irrelevant for determining when the "last use" of a lifetime will be. It is also another fundamental feature of the current version of rust that leaks are safe and sound and they cannot be considered unsafe/unsound without removing useful features (this was not an intentional design decision originally but see search term "leakpocalypse").

Combined, this means that just because the lifetime of a value typed `Guard<'a>` ended, doesn't mean that value actually got dropped; it just went away for some unspecified reason. This doesn't mean drops are unreliable or anything like that; even though leaks are not unsound, in "correct" rust code you pretty much have to intentionally leak for it to happen. But it does mean that if some code must run to prevent data races, you can't just put it in a Drop impl and trust it will run.

Contrast mutex guards, which unlock the mutex upon dropping. This is still perfectly sound in the face of leaks, because it fails open: if drop doesn't run, mutex stays locked, so no data races can happen.

I'm very excited about this. Excellent news.
Oh, this is great to hear, no longer need to rely on crossbeam for scoped threads.

Whilst i can imagine this is useful in a variety of situations, it is particularly useful when trying to merge async and synchronous code which can sometimes be quite messy. Sometimes it is necessary to have a top-level runtime executing and spawn an async task from some synchronous code, the only way I know of to do this is spawn a thread and execute it in a separate runtime, without the 'static lifetime requirement that scoped threads allow makes it much easier to manage this kind of code.

Nested runtimes are a pattern to avoid if possible but sometimes it's necessary for complex programs.

Very excited to see this land, thanks to the Rust team!