What is an actor model?

The actor model was proposed decades ago (1973) by Carl Hewitt as a way to handle parallel processing in a high performance network — an environment that was not available at the time.

As a quick summary an actor is a message processing instance that is handling messages from a message queue – one at a time. Each actor can modify their own private state, but can communicate to other actors through messaging only.

The shift from synchronous method calls to a dynamic, asynchronous message dispatching was revolutionary. According to Carl Hewitt the actor model was inspired by physics including general relativity and quantum mechanics. And it was also influenced by the programming languages Lisp, Simula and Smalltalk.

Overall, the actor model’s focus on isolation, message passing, and fault tolerance makes it a compelling choice for building concurrent and distributed systems, which are increasingly important in today’s computing landscape with multicore and multithreaded microchips.

So it makes sense many modern computer languages i.e. Erlang, Akka, Scala, Kotlin, Dart, Pony, C# and recently Swift contain compiler support for actors and other languages like Python and Rust provide actor based concurrency libraries.

Swift Actors

Swift’s actors are conceptually like classes that are safe to use in concurrent environments. This safety is made possible because Swift automatically ensures no two pieces of code attempt to access an actor’s data at the same time – it is made impossible by the compiler, rather than requiring developers to write boilerplate code using systems such as locks.

In the following we’re going to explore more about how actors work and when you should use them, but here is the least you need to know:

  1. Actors are created using the actor keyword. This is a concrete nominal type in Swift, like structs, classes, and enums.
  2. Like classes, actors are reference types. This makes them useful for sharing state in your program.
  3. They have many of the same features as classes: you can give them properties, methods (async or otherwise), initializers, and subscripts, they can conform to protocols, and they can be generic.
  4. Actors do not support inheritance, so they cannot have convenience initializers, and do not support either final or override.
  5. All actors automatically conform to the Actor protocol, which no other type can use. This allows you to write code restricted to work only with actors.

As well as those, there is one more behavior of actors that lies at the center of their existence: if you’re attempting to read a variable property or call a method on an actor, and you’re doing it from outside the actor itself, you must do so asynchronously using await.

Swift Actors behind the scene

At the time when actors were invented the key idea was that all messaging and all operations on actors have a delay and are asynchronous. You won’t get an immediate response for a message or operation on an actor – but need normally to wait for some time. From this point of view you could also look at actor’s as single threaded remote objects. This means several things:

  1. Actors are effectively operating a private serial queue for their message inbox, taking requests one at a time and fulfilling them. This executes requests in the order they were received, but you can also use task priority to escalate requests.
  2. Only one piece of code at a time can access an actor’s mutable state unless you specifically mark some things as being unprotected. Swift calls this actor isolation.
  3. Just like regular await calls, reading an actor’s property or method marks a potential suspension point – we might get a value back immediately, but it might also take a little time.
  4. Any state that is not mutable – i.e., a constant property – can be accessed without await, because it’s always going to be safe.

Swift Actor rules to remember

In practice, the rule to remember is this: if you are writing code inside an actor’s method, you can read other properties on that actor and call its synchronous methods without using await, but if you’re trying to use that data from outside the actor await is required even for synchronous properties and methods. Think of situations where using self is possible: if you could self you don’t need await.

Writing properties from outside an actor is not allowed, with or without await.

Why Swift needs actors at all?

The fundamental purpose of actors is straightforward: if you ever need to make sure that access to some resource is restricted to a single task at a time, actors are your tools to use! You still have Grand Central Dispatch (GCD) with their operation queues and you still have the generic Concurrency Tools from the Operating System i.e. Semaphore, Mutex and Locks. However it makes more sense to use actors within modern Swift application layer code. The Swift compiler will support developers to avoid you do bad concurrency mistakes.

Producer Consumer (PC) with Swift Actor model

In computing, the producer-consumer problem (also known as the bounded-buffer problem) is a family of problems described by Edsger W. Dijkstra since 1965.

We have “Producers” and “Consumers” sharing a “Common Buffer” of data. All the Producers and the Consumers will be running on their separate Threads – while producers are inserting and consumers are removing from the buffer. However the buffer has a maximum number of items it is available to store. So before another producer can write to the shared buffer he has to wait until another consumer did remove an item and vice versa.

Dijkstra found a solution that is synchronizing the consumer and producer thread. When the buffer is full producer thread is suspended when the buffer is empty consumer thread is suspended. He does this using his famouse Semaphore’s data structures – you will find implementation in my Swift playground.

PC Algorithm in Swift

The solution of producer consumer with the actor model is straight forward. Just notice the buffer acts as an actor between producer and consumer threads. While producer and consumer threads are sending push pop messages to this actor the actor will dispatch them from producer to consumer while protecting the internal data structure and also throttle (yield) producer and consumer threads when their read or write operations are too fast. Also checkout my Playground.

import Foundation

actor AsyncBuffer {
    
    var list: [Int]
    let size: Int
    
    init(size: Int = 10) {
        self.list = Array<Int>()
        self.size = size
    }
    
    func push(value: Int) async {
        while list.count == size  {
            await Task.yield()
        }
        // Postcondition: At least one free position in buffer
        assert(list.count < size)
        list.append(value)
    }
    
    func pop() async -> Int {
        while list.count == 0 {
            await Task.yield()
        }
        // Postcondition: At least one item in buffer
        assert(list.count > 0)
        return list.removeLast()
    }
}

class Executor {
    
    let id: String
    let buffer: AsyncBuffer
    
    init(id: String, buffer: AsyncBuffer) {
        self.id = id
        self.buffer = buffer
    }
    
    func run() async {
        assertionFailure("Needs to be implented by subclass")
    }
    
    func start()  {
        Task {
            while true {
                await run()
            }
        }
    }
}

class Producer: Executor {
    var counter = 0
    override func run() async -> Void {
        await buffer.push(value: counter)
        print("\(id) push \(counter)")
        counter += 1
    }
}

class Consumer: Executor {
    override func run() async -> Void {
        let nextInt =  await buffer.pop()
        print("\(id) pop \(nextInt)")
    }
}

var buffer = AsyncBuffer(size: 20)

let p1 = Producer(id: "P1", buffer: buffer)
let c1 = Consumer(id: "C1", buffer: buffer)

let p2 = Producer(id: "P2", buffer: buffer)
let c2 = Consumer(id: "C2", buffer: buffer)

p1.start()
c1.start()

p2.start()
c2.start()

tomkausch

Leave a Reply

Your email address will not be published. Required fields are marked *