3 ms·
Message passing is a type of mutable shared state — but one that's restricted in some important way to eliminate a certain class of errors (in Erlang's case, to
by Twey 7mo ago
Message passing is a type of mutable shared state — but one that's restricted in some important way to eliminate a certain class of errors (in Erlang's case, to a thread-safe queue with pairwise ordering guarantees so that all processing on a particular actor's state is effectively atomic). You can also pick other structures that give different guarantees, e.g. LVars or CRDTs make operations commutative so that the ordering problems go away (but by removing your ability to write non-commutative operations). The big win for the actor model is (just) that it linearizes all operations on a particular substate of the program while allowing other actors' states to be operated on concurrently.
Nobody argues that any of these approaches is a silver bullet for all concurrency problems. Indeed most of the problems of concurrency have direct equivalents in the world of single-threaded programming that are typically hard and only partially solved: deadlocks and livelocks are just infinite loops that occur across a thread boundary, protocol violations are just type errors that occur across a thread boundary, et cetera. But being able to rule out some of these problems in the happy case, even if you have to deal with them occasionally when writing more fiddly code, is still a big win.
If you have an actor Mem that is shared between two other actors A and B then Mem functions exactly as shared memory does between colocated threads in a multithreaded system: after all, RAM on a computer is implemented by sending messages down a bus! The difference is just that in the hardware case the messages you can pass to/from the actor (i.e. the atomicity boundaries) are fixed by the hardware, e.g. to reads/writes on particular fixed-sized ranges of memory, while with a shared actor Mem is free to present its own set of software-defined operations, with awareness of the program's semantics. Memory fences are a limited way to bring that programmability to hardware memory, but the programmer still has the onerous and error-prone task of mapping domain operations to fences.
- _mrinalwadhwa_ 7mo ago> a thread-safe queue with pairwise ordering guarantees so that all processing on a particular actor's state is effectively atomic > The big win for the actor model is (just) that it linearizes all operations on a particular substate of the program while allowing other actors' states to be operated on concurrently. Came here to say exactly those two things. Your comment is as clear as it could be.
- Groxx 7mo agoSince I am not familiar enough with Erlang to know, is that actually the same as concurrent code? I can certainly see how it would share many of the same logical issues, but does sharing an actor imply that you can have non-atomic operations on the actor, like inconsistent writes and reads? I was under the impression that it would at least be atomic because actors are single-threaded. It even guarantees message ordering, while memory does not in some widely used hardware architectures. You can of course rebuild data races inside Erlang with the right set of messages, but it's not surprising that you can emulate a Turing-complete computer in a Turing-complete language.
- Twey 7mo agoThe operations are atomic because of the single-threaded nature of actors. But the operations can be arbitrarily ordered, except for the pairwise ordering constraint (which mirrors the happens-before relation on memory within a thread). If your memory actor has operations like ‘read address’ and ‘write address’ then yes, you precisely get the behaviour we enjoy from shared memory: if you read one address A then address B someone might have changed both of them in the meantime, and if you write A then read it back it might have been changed in the meantime. The difference is that you're not limited to just ‘read address’ and ‘write address’ operations: the shared actor gets to define higher-level operations that are atomic over larger sections of code.
- Groxx 7mo agotbh I think "concurrently shared memory that has no data races" is pretty significantly different from practically every other example of shared memory out there. you'll get Mem's behavior in single threaded async system with immutable data (because another actor can't mutate the data you've already received), but I can't think of anything else. that misses on both concurrency and "shared memory" since it's not actually shared. it's shared-readable or something along those lines.
- Twey 7mo agoBut there are data races. Mem as I set it out above is an example of multi-threaded, mutable data. Another process can't alter the data you've received, indeed, just as in a shared-memory system another thread can't alter the data after you copy it out of shared memory into your registers or an unshared memory space. But if you're storing the data in shared memory (a.k.a. requesting it from Mem every time) then you are racing with other actors who have access to Mem.