I agree there are many aspects to this. One dividing line is whether we expect any user-level programming model changes (as the one that you mention with blocking in an actor) vs. implementation level benefits.
Just looking on the user-level changes, I don’t see the actor model as a good fit for allowing blocking. After all an actor as we have it in akka is basically mutable state that can be accessed by putting messages into a single mailbox for the actor. It shines when you need exactly that managed access to mutable state.
As Patrik said, the problem of blocking an actor is an actual problem. This is not just incidental but the performance /efficiency benefit of asynchronous execution directly comes from being able to react even if some other process is ongoing. If a use case doesn’t require that interleaving, probably another programming is more suitable than actors.
(That said, I understand that sometimes you really have that problem of having to send requests to third-parties and waiting for results before progressing. It’s unfortunate indeed that you need to resort to stashing to be able at all to receive an answer from that third party. We probably provide better APIs for this use case, as long as you allow unbounded mailboxes anyway.)
Viktor shared another view on that problem with just allowing blocking in this comment: https://twitter.com/viktorklang/status/1262000679360618496
how do I know whether a method-call will unschedule the current virtual thread?
For any kind of asynchronous process, it’s a problem if user code is always allowed to block/suspend because it might block this asynchronous process (a generalization from an actor) from making progress. One tempting “solution” might be to enable the process to capture that unscheduling and manage the continuation of that thread itself. However, that has potential downsides if the suspended stack acquired other resources, or read mutable state, etc. The problem is that if you just allow blocking, any piece of code must always expect that any function it calls might suspend a thread.
So, in very real terms this has similar implications as going back to single-threaded, blocking code which, of course, works, but has the well-known efficiency problems.
For that (and other) reasons, languages and frameworks usually require that potentially suspending functions are marked as such. E.g.
go func in go,
async fn in Rust,
: Future[_], or
: Task etc in Scala. All these require potentially async functions to be marked as such. That’s also a learning from typed functional programming in general: if you require marking the main operational properties with types, it will force programmers to more cleanly separate pure computational parts and parts with operational consequences (like talking to a database).
Coming back to the original problem of having to wait for a answer from a particular third party in an actor. The problem with the actor model here is that there’s only a single mailbox that receives all messages from all communication partners. This particular problem could be solved by having separate channels for talking to separate parties (like in go). So, there’s a lot of potential for coming up with about alternative programming models that could better fit certain use patterns.
Stack based continuation models like threads or fibers are great when you have many parallel linear execution threads (that may be intermediately suspended) that share nothing. For those kinds of instances (like a stateless web server that talks to a DB), it’s a great and efficient model. All the tricky logic of multiplexing threads and connections and state etc. have been pushed to the infrastructure. It doesn’t help so much with the more complex cases where you want to manage mutable state efficiently yourself.