> Blocking threads are a much more natural way of thinking about concurrency.
The (ideal) programming model for writing a thread vs an async task is identical, and indeed the most natural way of writing sequential flows that may incidentally be temporally separated. A blocking operation and an await are equivalent (in the absence of shared memory; in its presence they differ in that an async task on a single thread gets to assume atomicity of the code between awaits, but that's not too relevant here). I think what Joe is arguing against here is exactly splitting up your business flows into a bunch of explicit state machine transitions.
The difference is only in the caller. Callers of async tasks (à la Rust) have the right and responsibility of controlling how (and whether) the task they create resumes, while threads/BEAM processes (and ‘stackful’ futures à la Go, JavaScript, etc.) are answerable only to the great global event loop in the sky (sometimes known as the OS scheduler or the BEAM multiplexer, etc.). This makes it easier to avoid bugs that are about forgetting to drive a task, but harder to avoid bugs that are about forgetting to stop a task, or stopping a task wrongly; see structured concurrency. It also hurts composability a bit, because by construction there can be only one event loop and there's no way to decompose it.
You can see Erlang (or I suppose we should talk about BEAM more generally now people like Elixir) in terms of OS processes as an analogy, but it's just as valid and perhaps a little more accurate to think about it as a pool of async tasks with a global event loop, à la Go, where all blocking-looking code is actually async. It shares a common problem of such languages that because it can't interrupt real blocking code, e.g. FFI, you have to be very careful not to accidentally starve your other processes with such a call. This is made more obvious (but not really fixed) by languages that colour async functions differently, because the async colour acts as a promise (sadly not enforced by the language) that the function will be a good neighbour and not block for too long (whatever that means).
I agree that blocking threads is generally more natural / obvious. Cooperative multitasking was always done for performance reasons and more recently for simplicity (in single thread implementations with no locking) and/or scaling reasons.
Message passing concurrency is going even further though, and never really seems to have taken off wholistically. Again, primarily for performance reasons. When we do pass messages (across networks, too/from RAM or disk, etc) we usually add caching and other optimizations which make concurrency even harder to reason about.
The line is fuzzy though.
You might say that each RAM access is really a message over a bus, and so is each cache access. Conversely, you could write a message passing style key value store and create race conditions by using like shared memory.
Transactional memory is also 'message passing' like -> you bundle up access of shared memory into a single transaction 'message'.
A lot of shared memory code uses constructions like FIFO/queues which are really not too distinct from message passing except when ownership is shared or goes back and forth. Perhaps that's just an 'implementation' of message passing.
You made me curious about the spelling of 'holistic', given that it really is based on the concept of wholeness. It turns out:
- Holistic is formed from the Greek root hol-, meaning "whole". Like hologram.
- It's even closer to the English than it looks; modern whole descends from Old English hal (cognate to the Greek word), which also gives us hale and health. The W in the modern spelling is a spurious addition unrelated to the etymology of the word. You'd expect "hole".
- Who is different, and the W in that word is etymologically correct. (Old English hwa.)