I'm working on an rest API server backed by a git repo. Having an actor responsible for all git operations saved me from a lot of trouble as having all git operations serialised freed me from having to prevent concurrent git operations.
Using actors also simplified greatly other parts of the app.
Isn’t that just serializing through a queue, aka. the producer consumer pattern? Which I think the correct solution for most common concurrency problems.
For something to be an actor, it should be able to:
- Send and receive messages
- Create other actors
- Change how the next message is handled (becomes in Erlang)
I think the last one is what makes it different it from simple message passing, and what makes it genius: state machines consuming queues.
Agreed, (heirarchical if you must) state machines consuming queues and writing to queues via messages wins. If you are FP minded like me, then you are set up to cleanly separate IO to the edges and have a functional core imperative shell hexagonal architecture for less additional overhead thsn a standard java beans style OO logical design.
So you're just using actors to limit concurrency? Why not use a mutex?
This might be a question of personal preference. At the design stage I already find it more approachable to think in separated responsibilities, and it naturally translates to actors. Thinking about the app, it's much reasier for me to thin "send the message to the actor" than call that function that uses the necessary mutex. With mutexes, I think the separation of concerns is not as strong, and you might end up with a function taking multiples mutexes that might interfere. With the actor model, I feel there is less risk (though I'm sure this would be questioned by seasoned mutex users).
In this simple case they're more or less equivalent if the only task is limiting concurrency, but in general usage of mutexes multiplies and soon enough someone else has created a deadlock situation.
Extending it however reveals some benefits, locking is often for stopping whilst waiting for something enqueued can be parallell with waiting for something else that is enqueued.
I think it very much comes down to history and philosophy, actors are philosophically cleaner (and have gained popularity with success stories) but back in the 90s when computers were physically mostly single-threaded and memory scarce, the mutex looked like a "cheap good choice" for "all" multithreading issues since it could be a simple lock word whilst actors would need mailbox buffering (allocations... brr),etc that felt "bloated" (in the end, it turned out that separate heavyweight OS supported threads was often the bottleneck once thread and core counts got larger).
Mutexes are quite often still the base primitive at the bottom of lower level implementations if compare-and-swap isn't enough, whilst actors generally are a higher level abstraction (better suited to "general" programming).
Atomic operations, memory barriers, condition variables, thread/"virtual processor" scheduling are philosophically cleaner since they're what the specified hardware/OS concurrency model actually provides, and can implement all of locks, mutexes, structured concurrency, arbitrary queues, actors etc. etc.
I was implicitly including all those with mutexes in the last sentence, it might be easier to reason about each in isolation because you're an experienced programmer that can/want to reason about the details.
In practice you want to keep the sharp knives away from junior programmers in larger contexts, handling a few ones in a limited scope is ok but when you start to reason about larger systems, actors in concurrent contexts can be explained even to junior programmers in terms of conveyor belts of messages or a similar analogy.
I don't know if you heard the story of how C++ was used and a large part of the collapse of that project was due to C++, OO and/or threading issues and how Erlang was then used in a crash project to successfully write a replacement on the same HW.
Some people claim that functional programming and/or actor systems are inherently superior, I don't really agree on that assessment, it's more that the actor patterns were easily handled in a distributed system and when used by less seasoned programmers it was possible for them to create their parts in mostly "single-threaded" contexts + message passing without causing trouble by using lower level concurrency primitives.
Really, imagine 500+ mid-junior programmers banging away concurrent code on a larger project that needs to be shipped soon.
This is what I mean by philosophically cleaner.
You are using mutexes, they are on the Actor message queues, amongst other places. "Just use mutexes" suggests a lack of experience of using them, they are very difficult to get both correct and scalable. By keeping them inside the Actor system, a lot of complexity is removed from the layers above. Actors are not always the right choice, but when they are they are a very useful and simplifying abstraction.
Horses for courses, as they say.
Lock-free queues and 16-core processors exist though. I use actors for the abstraction primarily anyway.
Can you share some insights why mutexes are difficult to get correct and scalable?
Because actors were invented to overcome deadlocks caused by mutexes. See page 137. With mutexes you can forget concurrency safety.
I’m pretty sure it’s possible to deadlock an actor system.
A bad one, yes. But it was esp. invented to prevent the mutual exclusion problem. And good actor systems do so.