Thread-safe Actor on mobile

Introduction to Actor in Kotlin

Kotlin actors are a high-level tool in the Kotlin programming language for building concurrent and distributed applications. It provides a concurrency model based on the Actor model, which makes it easier to implement complex concurrency scenarios.

Actor model is a concurrent programming model, in which each component in the system is modeled as an independent Actor, which can receive and send messages, and can also modify its internal state. This model is very useful for processing asynchronous messages, and complex systems can be built through the combination of Actors.

The Kotlin actor library is built on this model. It provides an easy way to create and manage actors, and ensures asynchronous communication between actors by using coroutines as the underlying mechanism. This approach not only simplifies the programming model, but also achieves better performance and scalability.

In the Kotlin actor library, each actor is represented by a class, and an actor can be started by creating an instance of the actor. Once an actor is started, it starts waiting to receive messages from other actors, and can respond accordingly based on the received messages. When an Actor needs to send a message, it can use the send method to send a message to other Actors.

The Kotlin actor library also provides some additional functionality to help with concurrency scenarios. For example, it can support timeout and cancellation operations between actors, which can handle communication between actors more flexibly. In addition, the Kotlin actor library also supports supervision and hierarchical relationships between actors, making it easier to handle errors in the system.

Kotlin actors are a very powerful tool in the Kotlin programming language, which provides a simple and efficient way to build concurrent and distributed applications. If you are developing an application that needs to handle concurrent or asynchronous messages, then the Kotlin actor library might be a good choice for you.

Common APIs for Kotlin Actor

Kotlin Actor is a concurrent programming tool based on the coroutine mechanism, which provides a series of APIs to help developers handle concurrent tasks and message passing. Here are some commonly used Kotlin Actor APIs:

  1. actor function: Create an Actor instance and return an Actor reference. Messages can be sent to the Actor through this reference, and the cancellation of the Actor can also be performed through this reference.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.actor

suspend fun main() {<!-- -->
    val actor = actor<Int>(Dispatchers. Default) {<!-- -->
        for (msg in channel) {<!-- -->
            println("Received message: $msg")
        }
    }

    actor. send(1)
    actor. send(2)
    actor. send(3)

    actor. close()
}

An Actor instance is created using the actor function, and some messages are sent to the Actor. In Actor, we can receive messages through channel and process them. Finally, we close the Actor instance by calling the close method.

  1. send method: send a message to Actor
val actor = actor<String>(Dispatchers. Default) {
    for (msg in channel) {
        println("Received message: $msg")
    }
}

actor. send("hello")
actor. send("world")

Two string messages are sent to an Actor, which will be received and processed by the Actor.

  1. close method: close an Actor instance.
val actor = actor<Int>(Dispatchers. Default) {
    for (msg in channel) {
        println("Received message: $msg")
    }
}

actor. close()

Using the close method to close an Actor instance will stop the Actor’s message processing loop and release the Actor’s resources.

Usage example

Here we use Kotlin Actor to implement a message queue that supports priority, and each message carries a suspend function to indicate the specific tasks that need to be performed to consume the message.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.actor
import java.util.*

data class Message(val priority: Int, val content: String, val action: suspend () -> Unit)

fun main() = runBlocking<Unit> {<!-- -->
    val messageQueue = PriorityQueue<Message>(compareByDescending {<!-- --> it. priority })

    val actor = actor<Message>(Dispatchers. Default) {<!-- -->
        for (msg in channel) {<!-- -->
            messageQueue. offer(msg)
        }
    }

    // send messages to the queue with different priorities and actions
    actor.send(Message(1, "low priority message") {<!-- --> delay(1000); println("low priority action finished") })
    actor.send(Message(3, "high priority message") {<!-- --> delay(500); println("high priority action finished") })
    actor.send(Message(2, "medium priority message") {<!-- --> delay(750); println("medium priority action finished") })

    // execute the actions of the messages in priority order
    while (messageQueue. isNotEmpty()) {<!-- -->
        val message = messageQueue. poll()
        message. action()
    }

    actor. close()
}


Firstly, a Message data class is defined to represent the messages in the queue, including the priority and content of the messages. Then we created a PriorityQueue instance to store the message queue. Then we use the actor function to create an Actor instance to receive and process messages.

We have added a action attribute of suspend function type in the Message data class, which is used to indicate the execution action of the message. When sending messages to Actor, we specify a different action function for each message, and perform some asynchronous operations in this function, such as using the delay function To simulate some time-consuming operations.

In Actor, we add the received messages to the message queue, and then we execute the action function of the messages in the queue in order of priority by traversing the message queue, so that each message can be guaranteed Only after the carried suspend function is executed, it means that the message has been consumed.

Next, we sent three messages of different priorities to the Actor, and then output the contents of the messages in the queue in order of priority by traversing the message queue.

Finally, the Actor instance is closed by calling the close method

Actors in Swift

Swift 5.5 introduces a brand new Actor model that addresses many of the issues with multithreaded programming in Swift. Like Actor in Kotlin, Actor is a lightweight concurrency primitive that encapsulates a set of methods and properties in an independent runtime entity, which ensures thread safety and provides some convenient concurrency operate. In Swift, Actor is defined as a class type and modified with the @actor keyword. When an instance of a class is created, an Actor instance will be generated. All methods to access the instance need to be executed in the Actor execution context. to ensure thread safety.

Compared with traditional concurrency primitives such as locks and semaphores, Actor is easier to use, avoids many shared resource problems, and can better adapt to the asynchronous programming model in Swift. Actors can also communicate with each other through async and await keywords for asynchronous message delivery and processing.

Common APIs for Swift Actors

Commonly used APIs for Actors in Swift include:

  1. @ActorIndependent: An attribute modifier, used to identify an attribute is independent of the Actor and can be directly accessed in the external thread of the Actor without async/await calls.
  2. actor.receive(): Inside an actor, it is used to receive and process messages from other actors.
  3. actor.async: Inside the actor, it is used to send asynchronous messages to other actors, return a Task object, and wait for the corresponding response through the await keyword.
  4. actor.isolated: Inside the Actor, it is used to perform some time-consuming operations without blocking the Actor’s message processing loop.

The following is a simple usage example code showing an Actor receiving and processing messages:

@Actor class MyActor {
    var counter: Int = 0

    func receive(_ message: Int) {
        print("Received message: \(message)")
        counter + = message
    }
}

let myActor = MyActor()

// Send a message inside the Actor
myActor.async { actor in
    actor. receive(10)
}

// Access Actor individual properties outside of the Actor
print([email protected])

In the above example, we defined a MyActor class and implemented the receive method inside it to receive messages from other Actors and update counter attribute. In the main thread, we create an instance of MyActor and send it a message via the async method. In the async method, we need to obtain the Actor instance through the actor parameter to ensure execution in the Actor execution context. Finally, we directly access the Actor’s independent property counter in the main thread through the @ActorIndependent property modifier.

Usage example

We also implement a message queue that supports priority through Swift Actor, and each message carries an asynchronous method to indicate the specific task that needs to be performed to consume the message.

import Foundation

@available(macOS 12.0, *)
actor MessageQueue {<!-- -->
    private var queue = PriorityQueue<Message>()

    func addMessage(_ message: Message) {<!-- -->
        queue. enqueue(message)
    }

    func processMessages() async {<!-- -->
        while let message = await queue.dequeue() {<!-- -->
            await message. action()
        }
    }
}

@available(macOS 12.0, *)
struct Message {<!-- -->
    let priority: Int
    let content: String
    let action: () async -> Void
}

@available(macOS 12.0, *)
func main() async {<!-- -->
    let messageQueue = MessageQueue()

    // send messages to the queue with different priorities and actions
    await messageQueue.addMessage(Message(priority: 1, content: "low priority message") {<!-- --> await Task.sleep(1); print("low priority action finished") })
    await messageQueue.addMessage(Message(priority: 3, content: "high priority message") {<!-- --> await Task.sleep(0.5); print("high priority action finished") })
    await messageQueue.addMessage(Message(priority: 2, content: "medium priority message") {<!-- --> await Task.sleep(0.75); print("medium priority action finished") })

    // execute the actions of the messages in priority order
    await messageQueue. processMessages()
}

Task {<!-- -->
    await main()
}

In the above code, we first define a MessageQueue Actor class to store and process message queues. In MessageQueue, we use a PriorityQueue to store messages, and provide the addMessage method to add messages, and processMessages method is used to execute the asynchronous functions in the message queue in order of priority.

Then we define a Message structure, which is used to represent the messages in the queue, including the priority, content and asynchronous execution function of the message. When sending messages to Actor, we specify a different asynchronous function for each message, and perform some asynchronous operations in this function, such as using the Task.sleep function to simulate some time-consuming operation.

In MessageQueue, we add the received messages to the message queue, and then we execute the action functions of the messages in the queue in order of priority through asynchronous functions. In this way, it can be guaranteed that the asynchronous function carried by each message is executed before it indicates that the message has been consumed.

Same and Difference

Kotlin Actors and Swift Actors are implemented as Actor models in two different languages, which have some commonalities and differences:

Same point:

  1. Both are concurrent programming solutions based on the Actor model, which can achieve thread-safe concurrent operations.
  2. Both support asynchronous message delivery and processing, and you can wait for the corresponding response through the async and await keywords.
  3. Both provide some convenient Actor-related APIs, such as receiving and processing messages inside the Actor, sending messages to other Actors, and so on.

difference:

  1. Language implementations are different: Kotlin Actors are implemented based on coroutines, while Swift Actors are implemented based on asynchronous functions.
  2. Actors are defined and declared in different ways: Actors in Kotlin are decorated with the actor keyword, while in Swift they are decorated with the @actor keyword.
  3. Actors are instantiated in different ways: In Kotlin, Actors are based on coroutines, so each Actor instance is a coroutine and needs to be created through the actor() method; while in Swift, Actor is based on asynchronous functions, so each Actor instance is an object that needs to be created through the initialization method of the class.
  4. The message passing method is different: in Kotlin, you can send a message to an Actor through the actor's send method, and receive and process the message through the receive method; in Swift, you can pass the The async method sends an asynchronous message to an Actor, and the receive method receives and processes the message.

Actors and locks

When dealing with thread safety issues, the Actor mechanism and locks are two commonly used methods. Compared with the traditional lock mechanism, the actor model has the following advantages:

  1. Reduce lock competition: Traditional lock mechanisms are prone to lock competition when multiple threads access shared resources concurrently, resulting in performance degradation. In the actor model, each actor has its own state and message queue, and does not directly access shared resources, thereby reducing lock competition.
  2. Atomic operation: In the actor model, the state and message queue of each actor can only be accessed by itself, which ensures atomic operation and avoids data competition and concurrency issues.
  3. Asynchronous message passing: The Actor model implements thread-safe concurrent access through asynchronous message passing, thereby avoiding the overhead of lock waiting and release.
  4. Scalability: The Actor model can achieve high concurrency scenarios, because each Actor is independent and can run in different threads or different processes, thereby improving the scalability of the system.
  5. Simplified programming: Using the Actor model can transform the complexity of concurrent programming into a simple model of message passing, avoiding the complexity of manual lock management, and simplifying the difficulty of program design and maintenance.

Generally speaking, compared with the traditional lock mechanism, the actor model has more advantages in dealing with thread safety, which can improve the performance and scalability of the system, and also simplify the difficulty of concurrent programming. However, different concurrency scenarios and requirements may also require different concurrency solutions, which need to be selected according to specific situations.

syntaxbug.com © 2021 All Rights Reserved.