Is this implementation of the Actor model using project Loom correct?

Hello,

I love using mutable state Actors for their simplicity and performance.

I am currently using Akka and want to move to ZIO, which does not like mutability, therefore I cannot use it. I have implemented my own lightweight Actor class using Loom virtual threads. Here it is:

abstract class MyActor[T] {
	private val executor = Executors.newSingleThreadExecutor(Thread.ofVirtual.factory)

	protected def receive(message: T): Unit

	def !(message: T): Unit = executor.execute(() => receive(message))
}

I love it, and it seems to work in practice. However, I am worried about the memory visibility of the MyActor subclass constructor. I am worried that non-final (var in Scala) fields would not be memory-visible from the virtual thread.

From the Java documentation, it appears that ExecutorService.execute guarantees a “happen-before relationship”:

Memory consistency effects: Actions in a thread prior to submitting a Runnable object to an Executor happen-before its execution begins, perhaps in another thread.

Source: Executor (Java Platform SE 7 )

Can you please tell me if my implementation is memory safe, or must I also construct the class on the virtual thread Executor?

1 Like

I have done a lot of research today. I think it is safe - precisely because of the behavior of the Executor.execute mentioned above.

Any JMM experts to disprove me?

I think the ExecutorService.execute is memory safe. So this is a memory barrier for your actor!

BTW, I have also implemented an actor model that is memory safe using just CAS.

See: GitHub - otavia-projects/otavia: Your shiny new IO & Actor programming model!

1 Like

There’s no JMM issue, but this doesn’t seem consistent with the actor model. A key property of actors is that messages are handled sequentially within each actor. You’re executing receive concurrently from separate threads here.

I don’t think so? It’s executing receive within the context of the single-threaded Executor, so it looks like it should be shunting the messages onto that single thread of control. That looks compatible with the Actor model, precisely because of the choice of Executor.

scalafan - it looks safe for now. In a code review I’d ask you to do something to future-proof your var - to make your intent more explicit. Your overall system is concurrent; go ahead and fix the potential concurrency bug before you ever have it.

At a minimum - mark it with @volatile . It costs little more than the noise in your code in a single-thread system; it could save you a night of heisenbug stomping without you ever knowing it. Consider an AtomicReference or something like that if you need to check and hold the state while updating. If your actor is io-bound already then it is very unlikely that the in-jVM calls will ever be significant.

I hope that helps,

David

You’re absolutely right. I read too quickly (I read newVirtualThreadPerTaskExecutor and missed the fact that there’s one executor per actor). I withdraw my objection. This is actually quite neat, a good example of (conceptual) blocking that simplifies design.

@dwalend The var that you suggest protecting is in the implementation of a particular MyActor, therefore I technically don’t have control over it. A better solution would be to also run the constructor on the Executors.newSingleThreadExecutor. After this, there should be no safety objections anymore, as everything is going to be run on a single thread.

@charpov Yes, it’s a proof how modern technologies can replace huge complex systems, especially implementing trivial patterns like the actor model. Akka is absolutely bloated and and not at all a joy to use.

Yes, we went from “blocking threads simplifies designs” in my younger days, to “blocking hurts performance, let’s avoid it” to “we can have virtual blocking with minimal impact on performance”. See my reflections there: https://bit.ly/virtual-threads.

Two more things:

  • What’s the cost of queueing the lambdas (instead of the messages), memory-wise? Negligible?

  • I agree on Akka. I was never a big fan, and adding types has only made things worse (to my great disappointment, given how fan I am of strong typing!).

What’s the cost of queueing the lambdas (instead of the messages), memory-wise? Negligible?

With actor model, you don’t want to allow to queue arbitrary lambdas from outside the actor. Ideally, only immutable messages shall be allowed.

Of course, queuing internal lambdas from inside the actor should be fine (including delayed and periodic lambdas).

I agree on Akka. I was never a big fan, and adding types has only made things worse (to my great disappointment, given how fan I am of strong typing!).

I like strongly typed actors (especially with Scala 3, where you can represent union type as an allowed message). This is why my implementation above has a type parameter, rather than just Any. But not Akka implementation with that become() nonsense being forced upon you.

One thing I have realized once I got rid of Akka is how many exceptions I was ignoring and improperly handling with the weird Akka exception handlers and “supervisor strategy”.

Hi, thank you for posting the concept, I really like these “minimal libraries that have what I need”. I wanted to add two variants, just because I thought they might also be neat.

@FunctionalInterface
trait QueueActor[T] extends Runnable {

  private val mailbox = LinkedBlockingQueue[T]()

  def run(): Unit = while true do receive(mailbox.take())

  protected def receive(message: T): Unit

  def !(message: T): Unit = mailbox.put(message)
}


@main def exampleUse() =
  val y: QueueActor[String] = println
  Thread.ofVirtual().start(y)
    
  y ! "hello world"

It has the benefit that it does not hide the mailbox behavior and the receive loop in the executor. Which allows you to answer questions like: “if I send two messages one after the oter, will they be processed in that order?”. For the one in the initial post that’s not so clear.

Also, exceptions. They happen. With the above, it’s pretty clear what happens: loop terminates, actor stops processing. With the original proposal, I think an exception will only terminate a single receive method, thus leaving the actor in a potential inconsistent state.

You could also replace the run loop by just a step() method that does a single receive, which would easily allow you to integrate the design with some external event loop (say, in JavaScript, or when you don’t have Loom).

Also, there isn’t much reason to stick to Erlangs syntax if you want “execute method later, but keep the order of sequential calls intact” and still have normal method syntax (might be easier if you have many different methods/parameters, and you want to avoid creating message types for all of them.

trait InOrderAsyncMethods[T] extends Runnable {

  private val mailbox = LinkedBlockingQueue[Runnable]()

  def run(): Unit = while true do mailbox.take().run()

  protected def later(block: => Unit): Unit =
    mailbox.put(() => block)

}

@main def exampleUse() =
  object PrintActor extends InOrderAsyncMethods {
    def printString(s: String): Unit = later:
      println(s)
  }
  
  Thread.ofVirtual().start(PrintActor)

  PrintActor.printString("hello too!")

Lovely.

However, because a blocking loop only ever makes sense to execute on a virtual thread, I would definitely encapsulate the virtual thread inside of the actor, rather than having it separate like this.

On the topic of error handling, I actually wrap message handling in a try block, like so:

abstract class MyActor[T] {
	private val executor = Executors.newSingleThreadExecutor(Thread.ofVirtual.factory)

	protected def receive(message: T): Unit
	protected def onError(t: Throwable): Unit

	def !(message: T): Unit = executor.execute { () =>
		try receive(message)
		catch { case t: Throwable => onError(t) }
	}
}

There are valid reasons to have an actor on a single physical thread – when it controls an external resource that is not thread safe for example. Though that is probably quite niche.

Anyway, I did not want to imply that one thing is better than another, just more options :slight_smile:

I am curious what you think of my Actor project Leucine. It has typed actors too, from loose to strong enforcement. Btw, it does not make use of Loom (yet).