> When Rust was originally announced, the team had ambitions to pursue the multi-core MacGuffin with a green-threaded actor model, but they found out that it’s very hard to do green threads with native code compilation.
It's easy to do if you're willing to define certain synchronization points (such as I/O). IIRC, Rust abandoned Green threads because of a few reasons:
1. It doesn't play well with FFI. This is particularly true with segmented stacks, but Rust abandoned those before it abandoned green threading. With regular OS-provided stacks, FFI is a problem because the FFI call can't yield to the green scheduler. 2. It has an unavoidable performance problem even for code that doesn't use it. The existence of green threading means the entire runtime needs to be abstracted over a threading and I/O interface. This is a lot of complexity, and requires the use of dynamic dispatch for a lot of things that would otherwise be static dispatch. Rust originally switched to native as the default threading model but kept green threading as an option for a while, but this violates the concept of you-don't-pay-for-what-you-don't-use, especially since most people weren't even using green threading anymore. 3. It also made implementing some things very awkward. For example, because of the abstract notion of a runtime, you couldn't really implement selection over multiple file descriptors very well. Things like that must be handled by the runtime library or not at all, because for native threading you'd want to use select() (or kqueue or epoll) but for green threading the green threading library needs to deal with that. In Go, the standard solution there was to spawn multiple green threads that each block on one fd and communicate using channels (because the runtime can then collect all the blocked fds into a single select/kqueue/epoll), but that's unwanted overhead, and is especially not very good when you're not using the green threading. 4. Green threads encourage the use of lots of short-lived threads that have very shallow stacks. Rust abandoned segmented stacks with the understanding that the OS-provided virtual memory support was good enough that it didn't matter if every thread got an 8MB stack as it would allocate pages on-demand. But a 4k page for every green thread can be a lot of overhead if you're using thousands of green threads. You can of course explicitly request a smaller stack, but that's tricky to get right (if you request too little you explode). There was a lot of thought given to trying to statically determine how much stack space a given function call required (which is not always possible, and is especially tricky if there's any recursion) to deal with this, but that's a very hard problem.
Ultimately, it was determined that green threads simply weren't worth all the costs. The OS is pretty good about dealing with lots of threads, and arguably it's actually better than the green threading library is about scheduling them all. Green threading makes some sense if you want to spawn 10,000 simultaneous threads, but in nearly all real-world workloads it's just not really worth it.
> Rust has taken up a different challenge: eliminating crashes from otherwise traditional, “boring” multi-threaded programs. In the sense that Rust has abandoned its original vision in favor of pursuing more modest and achievable goals*
I find this an odd thing to say. Statically eliminating crashes (and data races) without a garbage collector or any performance penalty is actually significantly more interesting than actors. Rust did not decide to pursue the more modest "boring" goal, it redoubled its focus on pursuing the very hard (but achievable) and very important goal of solving the problem of memory/data safety in multithreaded programs. This aspect of Rust is what most excited me when I first got involved 2 years ago and what still excites me to this day and makes me wish I could use Rust in my day job.
> For the record, the Nim language manages to get this right, and the Rust folks might look to it for inspiration.
I've heard of Nim before but haven't actually looked at it. Does it attempt to solve memory/data race safety at compile-time like Rust does?
> It is also worth noting that processing non-overlapping slices in parallel is destined to come into mortal conflict with The Iterator, which is by its nature sequential
I don't think this is true. You can build an Iterator that yields non-overlapping mutable slices, and then you can process them in parallel. The iterator is sequential, yes, but you can call it multiple times and hand the resulting references to your parallel computations (as opposed to handing the iterator itself over, which doesn't work because you'd be sharing mutable state without a lock and Rust does not allow you to write that code).
Although it's worth noting that at the moment you can't do fork-join concurrency (so you can't share mutable slices with parallel computations like that unless you cheat with `unsafe`). There's `thread::scoped()` but it's marked as unstable (so you can't use it from Beta or from tomorrow's Stable 1.0) because it has a serious safety issue, and the proposed replacements are still being worked on.
Not in the general sense that Rust does. Nim has a (still experimental) parallel statement that can reason about some arithmetic operations and proves that array access is disjoint and thus safe.
I find it hard to believe you can read that and believe that Rust made this decision for anything but sound technical reasons.
* kernel space and time overhead is relatively gigantic * no cross-os usable kernel thread standard except fork() * when you are writing systems code, sometimes you either do not have a kernel, or are writing one * submitting to kernel threads means you must also give all cores over to the kernel as well, leading to competition between your program and others * still doesn't solve the core problem of cross-thread communication
If you're writing kernel code, you have to abandon most of Rust's standard library already, because there's lots of stuff that isn't available. This is one of the reasons why Rust's standard library is split into a bunch of different crates (that are all then re-exposed under the "libstd" facade), so you can pick and choose which bits of functionality you can actually use. If you're in the kernel, you probably can't use the user-space allocator, which means you have to drop liballoc, which means you have to drop most of the standard library. AIUI, kernels are pretty much constrained to using just libcore and then reimplementing other functionality as needed. Most relevant to this conversation, this generally means you have to drop the standard library's I/O and threading support entirely (userspace code that creates threads isn't really going to work in the kernel).
And at that point, there's absolutely nothing stopping you from implementing an actor model yourself. Heck, that's what the Rust standard library used to do! Although it used libuv, which presumably doesn't work in kernelspace, which means you wouldn't have even been able to use Rust's green threading from kernel code even if it was never dropped.
> submitting to kernel threads means you must also give all cores over to the kernel as well, leading to competition between your program and others
I'm not sure what you mean by this. A green threading implementation is built on top of OS threads anyway (using a small pool of threads roughly equivalent to one per core, with extra threads as necessary for the green threading implementation to do blocking tasks). There's nothing at all about green threading that lets you control a core all to yourself without sharing it with other processes. If your OS lets you request priority for a specific core, you can do that for OS threads just as well as you could with green threads.
> no cross-os usable kernel thread standard except fork()
Not to make your own point for you, but fork isn't available on every platform either. Windows has no fork. I'm not sure I consider forking analogous to threading anyways. They are both multiprocessing primitives, but I generally define threading (and it's derivatives) through how it's different than forking. That could easily just be me though.
> Another feature where a pragma seems out of place is inlining, although in this case I can’t really blame Rust for it. [...] I’ve always thought it should be up to the caller to say which functions they’d like inlined, and the function should just mind its own business.
It should actually be up to the compiler to make that call. And for all functions within a crate it is up to the compiler. Every function (that's not marked as `#[inline(never)]`) is a candidate for inlining anywhere else it's called within the same crate. The #[inline] attribute is a hint to the compiler that it might want to inline it more often, but the compiler is free to decide not to inline it.
Where #[inline] really matters is when calling it from other crates. The compiler can only inline functions that it actually can see the definition of. This should be obvious from the fact that inlining requires the compiler to copy the definition to the point where it was called. So what the #[inline] attribute does, besides hint to the compiler that you might like to inline it, is include the AST for the function in the library. This is actually the same thing it already does for generic functions (since generic functions need to be instantiated by the caller for arbitrary type parameters). So a generic function doesn't actually need #[inline] at all to be inlined. But non-generic functions do.
> (but five function calls (from_ptr, from_utf8, to_bytes, unwrap, to_string) just to convert a C string to a Rust string seems like an excessive amount of ceremony
You're right. The std::ffi module is relatively new. There used to be a simpler way to convert from a * const u8 into a &str but it appears to have fallen by the wayside.
> Rust won’t read C header files
You should check out https://github.com/crabtw/rust-bindgen. It's a tool that can generate the Rust extern interfaces from C headers.
> A function that does something unsafe (e.g. pointer arithmetic or calling C libraries) can be exported as safe.
If this weren't true than large swaths of the standard library would be unsafe, including anything that calls into libc or makes system calls.
When you expose a safe interface that uses `unsafe`, you're promising that you will guarantee safety yourself in a way the compiler cannot verify. If you do not want to (or cannot) guarantee that your interface is safe, then you should mark it as `unsafe` and let the caller deal with it. But the ability to wrap `unsafe` code in safe interfaces is absolutely crucial for lots of things, including much of the standard library. The hope is that most people writing Rust code won't ever have to use `unsafe` themselves because the standard library (or other people's libraries) will provide the necessary safe interfaces. But people writing safe interfaces around unsafe constructs need the ability to do this.
> but until a few days ago, Cargo didn’t understand linker flags
I believe this was already addressed in another comment, but this is not actually true. Cargo build scripts can provide flags to give to rustc, and `extern` declarations can be annotated with attributes specifying certain linker flags (such as the name of the library to link to).
> Iterators are a great and reusable way to encapsulate your blah-blah-blah-blah-blah, but of course they’re impossible to optimize.
I'm very curious to know where you got this idea from. Iterators actually optimize very well, in most cases being indistinguishable from a manual imperative for-loop. If you forget to turn on compiler optimizations then you'll see a significant performance difference, but that's true for a lot of different things you might want to do. Compiler optimizations are important whenever you're measuring performance.
> pragma
It's not a pragma. It uses the same # sigil that C compilers use to introduce pragmas, but in Rust, it actually denotes an attribute which modifies the next item (or in the enclosing item with the #! syntax). This is used for a number of things. As you've seen, it can be used to automatically derive implementations of traits, and it can be used to mark a function as being a candidate for inlining, among other things.
> Rust rather inelegantly overloads the assignment operator to mean either binding or copying.
This is indeed a very curious misconception. Assignment actually isn't overloaded like that at all. In your code example, when you say
You're not binding y to the value of x, you're actually moving the value of x into y. There is no implicit bind-by-reference in Rust. If you want a reference, you need to use the & sigil.The confusion here stems from the fact that some values can be copied and some values cannot. When you move a value in Rust, if the value can be copied (if it conforms to the Copy trait), then the original value is still usable after the move. This means you can say
The line `let y = x` moves the value of x into y, but since the value is copyable, it's really just moving a copy of the value. In this context, "copyable" basically means memcpy() can be used to produce a valid copy. There's a different trait called Clone which has an explicit .clone() method that is used for values that require additional work beyond memcpy() to copy.In your code example, your struct is not Copy, so moving its value makes the original value inaccessible. Basically, it's considered garbage memory and cannot be read from again. This is why your code
throws an error. It isn't because `y` and `z` would be referring to the same value, it's because after the line `let y = x;` the original value `x` is garbage.So really, any time you do assignment like that (or any time you pass a value as an argument to a function) without taking an explicit reference (using &), you're doing a move. Values that confirm to Copy will move a copy of the value, and all other values will leave the original value inaccessible. This should actually be familiar to people coming from C++, where values that aren't Copy are basically like std::unique_ptr, except that instead of leaving the original value with a known-"zero" state, the compiler prevents you from accessing the original value at all.