Hacker News new | ask | show | jobs
by symmetricsaurus 1636 days ago
> A key tradeoff is that Bronze does not guarantee thread safety

Not having data races is one of the key benefits of using Rust, “fearless concurrency” and all that. So by throwing away a key guarantee of Rust (no aliasing mutable references), it can become easier for learners to program in. It becomes a bit of an apples and oranges situation at that point.

7 comments

Yes pretty much like taking a statically typed language, making compile time type annotations optional and claiming victory that it's easier for beginners. Now they would get runtime exceptions instead.
It's still valuable to have a language that has better semantics and a more modern standard library than C++, even if it didn't get the benefits of strict memory management. That's still a Useful Thing.
Once you introduce a garbage collector there are plenty of other languages that provide that while still having expressive type systems and modern features, like Kotlin or C#.
Yeah, Rust's entire value proposition is that it doesn't lock you into GC
Rusts ownership system has benefits beyond memory management.

If I'm a situation where having a GC is ok and there is no "major library ecosystem benefit" (or simlilar) for one of the languages I still would choose rust over Python, JS, TS, Java, Kotlin, Dart, Scala (probably C#, idk. as I haven't used it).

The borrow checker is something which cost you once time to learn but if you are fairly familiar with it it normally won't cost you much time (if any). Sure there are still situations in which it can be tricky. But most times they are pretty clear and you can just throw a Clone/Rc/Arc at it and it's normally just fine (the borrow checker is still useful even with managed pointers/collections like Rc/Arc, in a certain way it makes them less error-prone to use, especially in case of more complex types like some thread safe Cow optimized manage pointer type you might find in a library).

Or… OCaml.
Isn't that language called D?
There are two forms of optional type annotations:

1. Using a placeholder (let/var/val/auto/...) -- e.g. in modern C#/Java/C++/Kotlin -- and letting the compiler figure out the type, but keep the actual type known at compile time. This will give you compile time errors when using the wrong types, and keeps the variables of a fixed type.

2. Effectively making all types variant types that can hold any value and can change their type -- e.g. in JavaScript/Python/Ruby -- such that they are dynamically typed. This can lead to runtime errors.

For languages like C#, Java and the derivatives, they have the concept where all objects are instances of a common type. If you use this -- especially in collections -- you can also get runtime errors. As long as you stick to the generic versions of these, the compiler will enforce the type safety.

The statically typed languages have been making types optional where the compiler can deduce them to avoid redundancy and duplication. If there is an ambiguity, the compiler will omit a compiler error. This is the best of both worlds -- type safety without the noise of annotating types everywhere.

My point here was that this was equivalent to using Object type in Java/C#. Here the data race guarantees of rust type system are elided due to this GC.
Rather it's like taking a statically typed language and making all type annotations inferrable.
If they were made inferrable, it wouldn't cause runtime exceptions. Here the data race guarantees of rust type system are elided instead.
True, yet in the context of the experiment that part of the language was not used, so they could compare the two approaches.

I think this research highlights a important strategy to make all software safer.

A quote from the discussion section:

> Encouraging adoption of safer languages by reducing stress.

It appears in this case that they're encouraging adoption of safer languages by making them unsafe and unsound: https://users.rust-lang.org/t/bronze-gc-and-aliasing-problem...
Usually, you don't start learning a new language with multithreaded operations..

In that sense, adding a gc to rust for single threaded programming, is almost useless : it's not helping that much and you're not learning.

> Usually, you don't start learning a new language with multithreaded operations..

When I have started with rust I was not writing stuff with Box/Rc/Arc... or even explicit lifetimes. Not saying that I was cloning everything but for simple stuff you can come long way with just moving and simple borrowing.

Depends if it's your first language, probably.

For an already experienced programmer, it's hard not to think about using multiple threads, even when just starting in the language. I know my few first Rust programs were using multithreaded constructs and I found that especially easy to do safely in Rust (when I started, there was already crossbeam and rayon, both making a lot of things easier).

Due to the type of software I do that is exactly how I look at new language: How it handles concurrency, what kind of synchronization primitives it offers, how it manages lifecycle etc. etc. If it does not provide enough facilities in comprehensible way then it essentially useless to me. I do not learn languages just for the f.. of it.
> Usually, you don't start learning a new language with multithreaded operations..

Why not? I mean, maybe not your first ever programming language, but - why should you not get used to doling out work to all available threads to begin with?

Because multi-threaded programs are largely unnecessary for solving many problems (as the popularity of python has aptly demonstrated).
If you already have the required experience for writing working multithreaded programs, then learning Rust isn't going to be an issue !

If you don't or are unsure about that, then stick to simpler forms of programming or use type-hinted Python instead.

This would the recommendation I would make to anyone asking me that question !

> Not having data races is one of the key benefits of using Rust

Exactly. Lifetimes, borrowing, etc. are the complexity of thread safety. It's like saying that airplanes are easy to fly, if you replace the airplane with a car.

Doesn't it seem useful to have one language that can be used at several levels of sophistication?
Depends on the tradeoffs.

Having different levels of abstraction within the same language can be really useful.

But you don't want to overcomplicate the language to support that, especially if it makes operating at different levels more complicated.

In this case, it seems like an odd tradeoff to make. The less sophisticated user is exactly the same kind of user that is more likely to foot-gun themselves with a memory or thread-safety issue. The "right" answer is probably a thread-safe garbage collector, but that has its owns set of usability and implementation tradeoffs.

How does Bronze overcomplicate Rust as a language? It's an optional library, not a language extension.

I think it's often pretty easy to not footgun yourself with thread or memory issues, because these issues simply don't exist in wide swaths of application.

It's locking you into single threaded usage, but you might very easy run into a library which requires Send bounds, as it uses e.g. rayon internally.

And for the cases where having managed pointers are better, Rc/Arc are often (not always) good enough.

Also it seems to be fundamentally unsound, the borrow checker isn't limited to making memory is freed correctly and safe multi-threading. But also affects assembly generation! (And is used to makes sure that all kinds of thinks work correctly, even without multi-threading).

The library seem to allow creating multiple mutable refs, which is instant UB in rust. I.e. having two mutable refs is already UB even if you only use one at a time. While this is just a PoC it means you can't implement it as a library! Only as a compiler extension which furthermore means a lot of optimizations must behave different if the extension used.. which is quite painful to maintain and prone to introduce compiler bugs.

Thank you for mentioning UB. I like Rust quite a bit, but once you trigger UB, things get nasty really, really fast. It feels like you’re using gcc at -O5 (not a real thing). It’s not like C/C++ in which you can sort of infer the consequences of UB on a single platform with enough experience.

It is greatly helped by compiler warnings, but; still, UB in Rust can be downright brutal.

The borrow checker does not affect code generation. Lifetimes are completely elided when codegen'ing.

What _does_ affect code generation is Rust's rules about borrowing and so.

If borrowck was affecting codegen, stuff like `RefCell` wasn't possible.

This reminds me of C# sharp and their introduction of the unsafe keyword. gc for everything until you need to manage your own pointers for speed or something.
Data races across threads, Rust type system does nothing to prevent data races across processes.
That's not entirely correct, or at least misleading. Rust will provide the same guarantees for variables in memory shared between processes as for variables in memory shared between threads. But you have to make sure that any locking datastructures you're using (like mutexes or read-write-locks) are able to work across processes.

(There are limits, though: if you map the same physical addresses to different virtual addresses, Rust can't help you. However, that is independent of threads/processes, because you can also do that in single-threaded programs.)

Which is a different story that just asserting fearless concurrency no matter what, also misleading.

Hence why I try to make a point that comes with a footnote.

Rust is after all supposed to target all kinds of system programming scenarios.

> Which is a different story that just asserting fearless concurrency no matter what, also misleading.

Frankly, you're being a bit disingenious. Nobody claimed that Rust can or will solve all conceivable concurrency problems. "Fearless concurrency" is generally understood to mean "...within a single program", not "...across different processes/machines/networks". By the time you understand what interprocess shared memory is, you're well able to correctly interpret Rust's "fearless concurrency" slogan.

Understood by most on the Rust community, not by others.

Most outside of the community aren't aware that nomicon points out exactly this.

By the way, there are also ways to cause havoc within a single program, example using a file as backing store being accessed by multiple threads concurrently, or accessing database data without transactions.

My goal is not to bash Rust, rather to trigger discussions around these kind of problems.

Both those situations are race conditions, but neither are data races. Rust only prevents data races, which is a specific kind of race condition, but it does not prevent race conditions in general.
When can data races across processes happen?

Are you talking about databases, services or IO and such?

I guess the simplest example is shared memory between processes.

Even Python has it: https://docs.python.org/3/library/multiprocessing.shared_mem...

Access to raw memory is locked behind the unsafe keyword though. Rust officially already does not guarantee any safety in that scenario even within 1 process.
There is always unsafe at some level on the standard library.

The point is that it doesn't protect the user of a crate that only exposes a fully safe API, unless they do digging to validate overall architecture safety.

>Even Python

It's not "even". Python specifically has it because it has no real threading.

Python does have real threading. The `threading` module provides os-level threads and synchronization primitives. The only difference between this and multithreading in C or Java is that CPython's GIL prevents more than one thread executing bytecode at a time. This prevents parallelism, but not concurrency.

Note this does not mean that python code is thread-safe by default. At most, you can theoretically rely on bytecode operations to be atomic, which means you'll need to synchronize multi-threaded code with mutexes, semaphores and higher-level synchronization constructs.

Python has cooperative threading. It's the same threading model used in the Erlang VM, Julia and many other dynamically typed languages. But preemptive threading vs. cooperative threading is orthogonal to whether data races can happen. Java threads are preemptive but data races can still happen.
The Erlang VM does preemptive scheduling.
SharedMemory is a new thing in Python. Not even supported by all 3.x versions.
But this is specifically about Rust.

What data races between processes, other than Disk/IO, databases, or external services, can a Rust program have?

I explicitely exclude the whole category of external services, since that is "by design" really. And the whole reason for ACID, global mutexes, transactions and CRDTs.

That and kind of data structure that can be shared via IPC mechanisms, some of them even transparent for the processes.
Environment variables.

Locales.

Quite a few other POSIX bits, really.

It is not possible to have a data race with environment variables across multiple processes. Every process has its own copy of environment variables (in fact they have their own copy of the entire environment).

I'm not sure what data race is possible across processes with locales, that's too vague of a claim to make.

One type of locales I know are the LC_ env vars. So there the "ENV is a copy" applies too.

Another would be to read and write into locale files, such as JSON. But then the ame applies as with any database or IO: this is inherently race-condition-prone and that is by design.

Maybe grandparent is thinking about locales in many web frameworks, that is some global var which should not be shared across users. So that if you set `Locale.current = "EN_GB"` that applies for any (email)notifications, errors, files, responses or such, being sent out during that request/response and during any jobs that request/response may spawn. In e.g. Rails this "somewhat global var" is a Frankenstein, but works suprisingly stable, actually.

It's interesting to see if one can come with a solution based on custom `Send`/`Sync`-like traits.

Of couse it will require nightly since auto traits are not stable.