This is an old blog post of mine, but I still really appreciate this subtle feature of Rust: one of the ways it supports robust thread-safe concurrent code is by _not making everything thread-safe._ It's odd that this property is so rare in languages. Just like how clear boundaries are critical in relationships, being able to designate which parts of your program _can't_ be shared across threads is critical for ensuring that the shared parts are correct.

https://cliffle.com/blog/not-thread-safe/

#rustlang

Safely writing code that isn't thread-safe

An under-appreciated Rust feature

I was musing on this because, as a for-fun thing, I'm rewriting some complex Go code in Rust, replacing ornate goroutine concurrency with compiler-managed concurrency with async... with no runtime.

It turns out the Go code is chock full of data races. I'm finding them because they're compile errors in the Rust code. So it's very nice to be able to draw lines around "this bit is async but not threaded" vs "this bit is not even async."

...unrelated to concurrency, I've noticed a class of Go footgun that I hadn't hit before:

// Foo contains a public Var field
type Foo struct { Var int32 }
// Bar contains a private var field and a Foo
type Bar struct { Foo; var int32 }

Bar is "embedding" a Foo, which causes it to do the equivalent of a Rust Deref coercion -- syntactically it now has a field called Var, in addition to one called var. (In the actual code I'm studying, this fact is far less obvious, spread across a couple of files and several dozen fields.)

These fields differ only in capitalization, which is Go's convention for public/private. And they're incredibly easy to confuse -- the code contains at least one bug resulting from this. The linter, which I am now running thanks to the lsp, is silent.

Anyway, check your capitalization!

@cliffle yes. Go's utter lack of concurrency safety has been a source of a number of production bugs in systems that I work on. Even despite use of the Go Thread Sanitizer. Every time it has been something that would be a compiler error in Rust.
@cliffle what does the Go data race detector report on that codebase? `go test -race`
@maxsz doesn't say anything different compared to go test. (I'm also not a big fan of these detector tools that teams have to remember to run.)

@cliffle it took me a while to even notice that JS (ECMAScript, to be more precise) is rare among dynlangs, to limit itself to single-threaded GC, comparable to Rust Rc+Cell

IME, most ways to split JS workloads across threads (whether in the browser or node.js/Deno etc.) still rely on inter-process-like mechanisms such as message passing

(and even `SharedArrayBuffer` is designed like cross-process shared memory! you need to combine it with wasm to simulate multi-threaded memory unsafety)

@cliffle now for a slighty hotter take:

C implementations could've, when adding threads, make all global state thread-local by default (w/ an opt-in for cross-thread sharing)

so C threads would default to being like processes sharing an address space

AFAIK, on (some?) RISC ABIs, globals and thread-locals each reserve one base register, and it's mainly register-starved CISC ABIs which treat them differently (also dynamic linking loves to make a mess of everything, but that's another story)

@eddyb at least the most common register-starved CISC (x86) actually _does_ support this, because of its historically-goofy segment register prefixes. You can use FS and GS for this.

Unfortunately circa 2010 some of those segment register accesses started to be emulated by a slow path in microcode in a lot of Intel parts (particularly Atom), which made things a lot harder on NativeClient where we were attempting to use them for stuff.