What concurrency model do you prefer? Seems like it’s easy enough to opt into a share-nothing model in Go, and this is generally what I do unless I have performance concerns (and as a consequence, I very rarely see concurrency bugs). I’m also not sure how much static analysis a la Rust could help the problem without accepting Rust levels of productivity.
The only foolproof way to opt into shared nothing in Go is not to spawn any goroutines. Global variables (which are all mutable, because there aren't any other kinds of variables in Go) are used all over the place in Go, including in the standard library.
How many of the concurrency bugs in this paper trace to the combination of "share-nothing" designs (channels) with mutable global variables in the standard library? If the answer is "none", what evidence does this paper present that mutable globals in the standard library are a practical impediment to "share-nothing" designs in real programs?
I mean, standard library global variables not being thread-safe would generally be treated as bugs in the standard library, so I wouldn't expect to see bugs there.
The point is that shared-nothing designs are just not how Go typically works. You can see that in functions like http.HandleFunc() (and everything else that uses the DefaultServeMux), which registers a global handler across all threads of the program.
Is that a good example of impediments to correctness in shared-nothing designs in Go? Using the default mux makes it hard for two different services to share the same Go program, and reduces the flexibility of libraries, but it doesn't appear to harm correctness or drag programs into shared memory designs. It seems like more of a purity test than a practical critique.
But the default serve mux is shared memory (it's literally a mutable global variable in net/http). If it isn't shared memory, what is?
I mean, Go doesn't force you to use shared memory; no language with threads does. It encourages it through library design (e.g. the default serve mux) and language design (e.g. package init() functions) though.
Right, I'm not disagreeing with you that the Go standard library and a few idiomatic Go standard library transactions use shared memory. I'm disputing that these are real impediments to building programs that benefit from shared-nothing designs. Obviously, Go does a better job of minimizing shared memory than eliminating it. I'm asking: does this distinction matter in practice?
There's Go functionality such as pprof that requires the use of the default serve mux and therefore you can't use pprof with a true shared nothing design. The log module is similar.
Does that lead to more bugs in programs? I don't know. It's quite possible it doesn't matter in terms of defect count. It's not shared nothing, though, is all I'm saying.
Right, thus the distinction I'm making between purity test concerns and practical concerns. Go is not, and has never claimed to be, a pure share-nothing design. It's pretty up front about not being that!
Right, but I don’t need “foolproof”, especially if it comes at high costs. I can already get mostly correct code in Go and the time I spend debugging is less than the overhead in other languages.
The big advantage of actors are that they are compatible with network loss. Erlang and Akka actors are network transparent. Local-only actors are missing the point, IMO.
I'm fairly confident that Erlang's "actors" (the language authors didn't know about Hewitt's work at the time) were originally local-only, since the objective was robust processing on standalone network routers.
I suspect the fact that asynchronous messaging turns out to be particularly well-suited to network communications was a happy accident.
Anyway, I guess my point is that local-only actors can be useful, but I definitely agree that network transparency is a huge win.
It's one advantage but not the only one. Actor systems can greatly increase throughput and scalability with easier code without complex synchronization. There are plenty of benefits running only on a single host.
I've not used an actor model--is there some router component that resolves the process to which a message needs to be sent? Is every send() operation a tuple of `(address, message)`? If so, presumably the router component decides whether there is a local actor or whether it needs to go out over the network?
Erlang runs atop its own VM that includes scheduler, routing services, etc.
There are (at least) a couple of different ways to identify the recipient of a message: process ID (unique identifier for another actor) or Erlang's name service.
The VM does indeed know whether the recipient is local; the sender typically neither knows nor cares, although the information is available if useful.
In Erlang, you do send a message to a destination. It also offers a way to register a name to a process, which you can build on to make whatever routing you need. Erlang/OTP ships with a module called pg2 that builds a synchronized group of processes in your distributed system, sending to the group will by default pick a local process over a remote process, or you can broadcast to all the processes.
Every 'proc' (like a thread) is id'd. You're responsible for finding the proc you want. Some procs are globally named and there can only be one. The VM translates proc IDs when you message pass across the network. If the proc dies, your message will fail, so you'll have to find the new target or drop what you were trying to do.
How do you make optimal use of a computer's memory hierarchy? Do messages allow structural sharing of data? Or is every message assumed to potentially go over the network, hence destroying opportunities for optimizations (e.g. think of the internal workings of a high-performance database engine).
But the idea is not that you as the end programmer "make optimal use of a computer's memory hierarchy".
It's that you write in a way that makes it easy for the runtime to ensure what you do is solid and correct and scalable -- and it's up for the runtime makers to make sure it makes optimal use of the computer's memory hierarchy.
And it's also that you sacrifice some of that "optimal use", to get the "solid and correct and scalable" part.
Iirc the idea here is that there are four very smart engineers working on the core that have taken care of that for you. Everything on top is quite battle tested.
Erlang does have a shared binary type, but the other types are simply copied. This is likely not space efficient, and it likely uses a lot more memory bandwidth. However, this allows for the garbage collection to be extremely simple. It may be pretty useful for NUMA systems as well; although, I'm not sure how well the VM manages that, it may be more potential than actual.
> I’m also not sure how much static analysis a la Rust could help the problem without accepting Rust levels of productivity.
You're implying that Rust has productivity issues, which is not true. I'd say I'm as productive in Rust as I was in C# and more productive that I was in JavaScript. Productivity isn't a problem in Rust, the true issue is the level of involvement and effort needed to learn the language in the beginning.
I would not cite Rust as proper static concurrency analysis, but Pony.
Pony is the only fast language (in use) with safe and proper static concurrency analysis and support for shared memory.
And there exist many other concurrency-safe slower languages with message-passing only, which don't have the Go problems. Go has at least the tools to detect deadlocks at run-time in testing, but these tools do not replace proper static type-checks for concurrency bugs.