Always reminds me of this quote:
"Any sufficiently complicated concurrent program in another language contains an ad hoc informally-specified bug-ridden slow implementation of half of Erlang." - Robert Vriding
Always reminds me of this quote:
"Any sufficiently complicated concurrent program in another language contains an ad hoc informally-specified bug-ridden slow implementation of half of Erlang." - Robert Vriding
The problem is when one tries to make a general purpose solution out of that design.
For example, how would Erlang look like in GPGPU programming, video/audio codecs, graphics programming, GUI,...
yup. go(lang) / rust seems to be exploring some ideas with an almost similar approach, but not _solely_ focussed on n/w programming. are there other languages out there as well ?
and a slightly open ended question as well: in your opinion, do you think a language needs to implement a complete runtime (similar to erlang) with it's own 'process' abstraction, scheduler, i/o etc. etc. to provide concurrency support ?
The main difference across those languages and Erlang/Go is how they expose the runtime to the developers.
It isn't a black box, rather they exposes a default set of behaviors and as set of APIs that allow to fine tune its behaviors.
wasn't it hans-boehm, who famously posited (?) that concurrency needs to be part of the language, rather than bolted on after the fact ? do you think that makes all these other languages kind of poorer w.r.t concurrency ?
edit-001: specifically, this is the paper that i am talking about: http://www.hpl.hp.com/techreports/2004/HPL-2004-209.pdf
What you are missing is that those languages are quite expressive, thanks to generics, operator overloading, trailing lambdas, and APIs into runtime the are available as tools for the programmer.
So you get, for example, async/await built into the language, with a default behavior. But at the same time, the necessary runtime hooks that allow any library writer to fine tune and built other concurrency models on top of it.
For the user of those concurrency models, it is indistinguishable where they are coming from.
Those are design decisions you can't bolt on later. Isolation, prescheduling and message passing without shared memory get you access to all of that.
It is only a matter of making use of the said compiler hooks and underlying machinery.
Mimicking the patterns doesn't replace the VM.
There are for example, distributed agent programming languages built on top of them, including network aware distributed GCs.
So for example, in that paper, 4.1 is about the problem of concurrent modifiability. And indeed, it says
> Indeed, under the implementation strategy we outlined above, in which the compiler is unaware of threads, it is allowed to transform code subject only to sequential cor- rectness constraints and hence could generate the code con- taining a race.
However, in Rust, this re-ordering can't happen: Rust won't let you alias x and y between two threads without some sort of synchronization primitive. But this isn't because Rust knows about concurrency, it's because Rust knows about aliasing. In a sense, Rust-the-language makes this program _impossible to write_, but a library re-enables you to write this program. You need unsafe to do this, but it's all wrapped up inside of the implementation of, for example, Mutex<T>.
From the last part of this section:
> Resolving it essential requires a programming-language-defined and compiler-respected memory model, simply to ensure that the user and compiler can agree on when there is a data race.
We're in agreement here, but the model is built around aliasing, not concurrency.
4.2 is about speculatively executing store instructions. I know less about this, but again, it's built on the idea of two threads accessing data at the same time, unsynchronized. This can't happen in Rust due to the aliasing rules.
4.3 is about register promotion. This cannot happen in Rust, because you don't call a function to acquire the lock, then do whatever you want. Mutex<T> hides the value it's locking inside of itself, unable to be accessed from the outside, and the call to acquire the lock returns a mutable reference to the inner data. The call to acquire the lock is the only way to get said reference, and Rust's aliasing rules will forbid any other kind of access through the returned reference. So this kind of transformation can't happen in Rust either.
Section 5 is about performance. It's true that synchronization primitives are expensive. Rust can again use unsafe code in a disciplined way to provide safe concurrent modification, while ruling out data races entirely. For example, consider a simple map operation. We take an array of integers, and for each element, add one to it. This is an embarrassingly parallel operation, yet, as the paper mentions, with a pthreads-style approach to making it safe, one would need either a single lock around the whole array, which destroys the concurrency entirely, or some set of more fine-grained locks, which introduce cost, as well as limiting the amount of concurrency to some degree.
But with a small utility function [1], which performs a small (ie, non-atomic) check at runtime, we can safety split up our array into as many disjoint chunks as we'd like, and then pass each one off to its own thread, which is free to do the modification with no more synchronization needed. In fact, libraries like Rayon can even determine roughly the correct amount for you, if you don't want to think about it, and it will near-transparently just handle this for you (you change a call from iter() to par_iter() and you're done).
1: https://github.com/rust-lang/rust/blob/f8614c397313db00e4b46...
So yeah. I'm in agreement with the paper that the language needs to do _some_ kind of reasoning, but since aliasing and concurrency are so tightly related, I would argue that the language could understand only aliasing, not concurrency, and then library abstractions are sufficient.
For GPGPU, video/audio codecs and all ... You want single threaded things. And there Erlang says "i'm ok with that" and let you use Native Implemented Functions (NIFs) that enable you to use Rust, C and co. Or to use what is called a Port and let you interface with a program outside of erlang in a safe way. Works quite well.
Then Erlang is used as a "concurrent and distributed glue" between your programs. There are a couple of scientific simulations that use erlang like that in the wild.
1) it was never seen as a problem because the erlang team believe (and i agree with them) that you best let specialists deal with their speciality are.
2) There were never an efficient JIT on the BEAM. This is something that is regularly looked at by the OTP team, but it would be a massive effort, just to try to approach effiency of what other language, that you can easily interact with, could do.
3) The use case are small. Let be honest. The amount of problems that are really only CPU bounded are limited in day to day work.
Parallelism is about performance, and it seems contradictory to do parallelism well on top of a slow language. Yes, Erlang can call out to other languages, but so can shell scripts.
Some other examples are live code loading, which means you can upgrade a system without restarting or interrupting it, the extensive tracing and statistics, etc. All these little things make it very nice to build and maintain a long-running service, but add up to make the VM slower. If you're doing batch jobs and have no realtime requirement, then the trade-off may not be so attractive.