Remix.run Logo
shadowgovt 5 days ago

This is, in my mind, the trickiest issue with Rust right now as a language project, to wit:

- The above is true

- If I'm writing something using a systems language, it's because I care about performance details that would include things like "I want to spawn and curate threads."

- Relative to the borrow-checker, the Rust thread lifecycle static typing is much more complicated. I think it is because it's reflecting some real complexity in the underlying problem domain, but the problem stands that the description of resource allocation across threads can get very hairy very fast.

pornel 5 days ago | parent [-]

I don't know what you're referring to. Rust's threads are OS threads. There's no magic runtime there.

The same memory corruption gotchas caused by threads exist, regardless of whether there is a borrow checker or not.

Rust makes it easier to work with non-trivial multi-threaded code thanks to giving robust guarantees at compile time, even across 3rd party dependencies, even if dynamic callbacks are used.

Appeasing the borrow checker is much easier than dealing with heisenbugs. Type system compile-time errors are a thing you can immediately see and fix before problems happen.

OTOH some racing use-after-free or memory corruption can be a massive pain to debug, especially when it may not be possible to produce in a debugger due to timing, or hard to catch when it happens when the corruption "only" mangles the data instead of crashing the program.

shadowgovt 5 days ago | parent [-]

It's not the runtime; it's how the borrow-checker interoperates with threads.

This is an aesthetics argument more than anything else, but I don't think the type theory around threads and memory safety in Rust is as "cooked" as single-thread borrow checking. The type assertions necessary around threads just get verbose and weird. I expect with more time (and maybe a new paradigm after we've all had more time to use Rust) this is a solvable problem, but I personally shy away from Rust for multi-threaded applications because I don't want to please the type-checker.

pornel 4 days ago | parent [-]

You know that Rust supports scoped threads? For the borrow checker, they behave like same-thread closures.

Borrow checking is orthogonal to threads.

You may be referring to the difficulty satisfying the 'static liftime (i.e. temporary references are not allowed when spawning a thread that may live for an arbitrarily long time).

If you just spawn an independent thread, there's no guarantee that your code will reach join(), so there's no guarantee that references won't be dangling. The scoped threads API catches panics and ensures the thread will always finish before references given to it expire.

shadowgovt 4 days ago | parent [-]

I'll have to look more closely at scoped threads. What I'm referring to is that compared to the relatively simple syntax of declaring scopes for arguments to functions and return values to functions, the syntax when threads get involved is (to take an example from the Rust Book, Chapter 21):

  pub fn spawn<F, T>(f: F) -> JoinHandle<T>
      where
          F: FnOnce() -> T,
          F: Send + 'static,
          T: Send + 'static,
... yikes. This is getting into "As easy to read as a C++ template" territory.
steveklabnik 4 days ago | parent [-]

The signature for scoped threads is both simpler and more complicated depending on how you look at it:

https://doc.rust-lang.org/stable/std/thread/fn.scope.html

But really, that first type signature is not very complex. It can get far, far, far worse. That’s just what happens when you encode things in types.

(It reads as “spawn is a function that accepts a closure that returns a type T. It returns a JoinHandle that also wraps a T. Both the closure and the T must be able to be sent to another thread and have a static lifetime.”)