1. 15
  1.  

  2. 7

    This is typical for “actor” libraries in languages with async/await. It gives you a very different programming model than that of Erlang: a model where it’s easier to express concurrent processes, but much harder to manage concurrent access to the state correctly (https://matklad.github.io/2021/04/26/concurrent-expression-problem.html).

    Prescriptivist in me screams that calling this “actor model” is wrong, but realist feels that it’s more productive to use something like “strict actror model” for things like Erlang or https://uazu.github.io/stakker

    1. 4

      I feel like in Erlang/Elixir you would just not put the authorization in an async block. The whole point of “actors” is that it’s a “unit of concurrency”. If you hadn’t put authorization in an async block, then it would be obvious that nothing could reenter. (In elixir, if you do a stdlib Task.async call that spawns a thread still nothing could reenter, so long as your await is in the same function block)

      I guess that’s not the case in swift? If you perform an async call, it instantiates suspend-back-to-the-actor points that you didn’t ask for? Man, what a footgun.

      1. 2

        There are two different approaches to re-entrancy in actors, and IMHO each comes with its own footguns.

        In the Erlang style (which I admittedly have not used), actors can easily deadlock by calling each other in a cycle. That kind of nastiness is one of the reasons I switched to using (async-based) actors.

        1. 6

          I’ve been doing elixir for four years now and I can count on my hands the number of times I’ve deadlocked, and 0 times in prod.

          Part of it is that genserver comes with a sensible timeout default, and part of it is that you just usually aren’t writing concurrent code; your concurrency is much, much higher level and tied to the idea of failure domains.

          If you’re running into deadlocks that often in BEAM languages *especially elixir, which gives you task, which shouldn’t deadlock unless you do something way too clever) you’re probably overusing concurrency.

          1. 3

            Actors can’t deadlock unless you allow selective receive (which is not part of Hewitt’s original model), but it’s difficult to imagine a pragmatic actor implementation that doesn’t allow it.

        2. 4

          Prescriptivist in me screams that calling this “actor model” is wrong, but realist feels that it’s more productive to use something like “strict actror model” for things like Erlang or https://uazu.github.io/stakker

          It’s funny because apparently the creator of the actor model says that Erlang doesn’t implement it and the creators of Erlang agree.

          Edit: Eh, that might have come off as snide. I mean to say it’s funny because it feels like a no true Scotsman fallacy.

          “Akka implemented the actor model in Scala.”

          “Well, that’s a library! Swift made it a part of the language!”

          “Swift allows suspend points within it’s actors. Erlang is the strict actor model”.

          “No, Erlang doesn’t even implement the actor model! But Pony does!”

        3. 4

          The author buried the lede pretty deeply beneath a bunch of example code; here’s the meat of the article:

          actor reentrancy prevents deadlocks and guarantees forward progress. However, it does not guarantee that the actor’s mutable state will stay the same across each await.

          I’ve run into this in my own homemade C++ actor implementation, and it can happen anywhere you’re using async function calls or even callbacks.

          Using async doesn’t completely free you from thinking about race conditions and reentrancy; but it does limit the places where such conditions occur, making them easier to deal with.

          1. 3

            When I worked on Windows we had exactly the same reentrancy problem in single-threaded window message pumps and COM single-threaded apartments (which used the window message pump to handle RPC, a truly glorious hack). The problem always arises whenever you allow “suspension points” inside event handlers invoked from and running on a single-threaded event loop.

            1. 2

              Neither of the suggested fixes spark joy in me, it feels brittle. I’d probably reach for a serial dispatch queue to prevent overlapping operations in this case, which ironically enough would be more like the traditional actor model. In Rust I’d put the balance in an async mutex and call it a day, but Swift seems to shy away from this approach. I’m not very familiar with the new Swift syntax - is there perhaps a better way to design this that’s sympathetic to the new concurrency syntax?

              1. 1

                The strawman code here seems pretty awful. Of course if you check a property, sleep an arbitrary time, then wake, that property might be different? Why is this being labelled as a “problem” with actors?