The power of Actor Model and the effect of Pykka in Asynchronous Programming at Python

9 min read

Traditional programming models often fall short in a world where responsiveness and scalability are paramount. Asynchronous programming has emerged as a game-changer, enabling developers to build highly concurrent and responsive systems. 

zen8labs team understands the importance of leveraging cutting-edge technologies to deliver efficient software solutions. In this blog post, we will explore the Actor Model and delve into the impact of Pykka, a powerful Python library, on asynchronous programming.

Example of Actor Model

To learn about the actor model, let’s start with a concrete example. 

Picture a scenario in which an e-commerce website encounters a surge in demand for a particular product that has a limited quantity. In this situation, numerous users are simultaneously vying to purchase the product, with each request represented by a distinct thread. These threads engage in fierce competition to acquire one of the few available products. 

The beginning of the actor model

In order to address the issue mentioned above, it is necessary to ensure system consistency by implementing a mechanism that allows only one thread to access the buyProduct method at any given time, while the remaining threads wait for their turn. In the absence of the synchronized, multiple threads can access the buyProduct method simultaneously, leading to inconsistencies. For example, let’s assume there are 10 products in stock. If threads A and B enter the method simultaneously and both see the stock count as 10, A may purchase 6 products, reducing the stock count to 4. However, when B attempts to purchase 5 products, it will encounter an error and can leave the user of thread B confused and frustrated. 

While the synchronized prevents multiple threads from entering the buyProduct method simultaneously, it introduces a bottleneck in the system. This means that several threads have to wait for their turn to access the method. This serialized fashion of accessing a method can be costly in terms of performance, resulting in deadlocks and underutilization of modern multi-core machine resources. 

To address this issue, relying on distributed locks may seem like a solution, but they come with their own complexities and limitations. They can be challenging to implement, limit scalability, and lead to increased application latency. 

Instead, a good approach is to leverage database transaction isolation levels. These levels, which fall under the “I” (Isolation) in ACID1, ensure that transactions remain isolated from each other, thus maintaining system consistency. 

This article explores how the actor model can be used to create high throughput distributed systems that handle concurrent traffic in a non-blocking manner. The actor model is specifically designed to be effective in multi-threaded applications on machines with multiple cores, as well as in distributed environments. 

By using the actor model, we can prioritize our business logic without being overwhelmed by low-level concerns like threads, atomic variables, locks, and other similar issues. This approach allows us to write code that is both high-performing and fault-tolerant, while ensuring consistent behavior. 

To gain a better understanding of this concept, let’s delve further into its details.

Understanding the Actor Model 

The Actor model is a paradigm that emphasizes the creation of concurrent and distributed systems by treating actors as independent entities. An actor is a fundamental unit of computation that encapsulates state, behavior, and messaging. This model promotes isolation, as each actor operates on its own local state and communicates with other actors exclusively through message passing. Model aimed at simplifying the design and implementation of highly concurrent systems. 

The Actor model is a paradigm

Actors have the ability to exchange messages with other actors, as well as create a hierarchy and define specific behaviors for the messages they receive. They serve as a container for state, behavior, child actors, and a message queue, or mailbox. 

Each actor in the system operates in isolation, ensuring the privacy of their internal state. Communication between actors is solely achieved through message passing, where messages are queued in the receiver actor’s message queue. This model presents a more efficient and flexible approach to writing concurrent code compared to the traditional thread-based approach, as it promotes scalability, resilience, and lightweight execution. 

The concept closely resembles that of object-oriented languages, where an object is invoked with a message and performs a specific action based on the received message (method call). However, the key distinction lies in the complete isolation of actors from one another, ensuring they never share memory. Additionally, it is important to highlight that an actor can maintain its own private state, which cannot be directly altered by any other actor. 

Even though multiple actors can run concurrently, a single actor will handle messages sequentially. This implies that if you send three messages to the same actor, it will process them one after the other. In order to ensure concurrent execution of these three messages, you must create three separate actors and send one message to each. Asynchronous messaging allows actors to receive and store messages in a mailbox while processing another message. The mailbox serves as the storage location for these messages.

At zen8labs, the adoption of the Actor model has proven to be a game-changer for building highly scalable and fault-tolerant systems. By breaking down complex functionalities into smaller actors, the development team can model the real world more accurately, leading to improved code maintainability and simplicity. This approach also facilitates the design of resilient systems, as the failure of one actor doesn’t compromise the entire system’s integrity. 

We will explore the advantages and disadvantages of the actor model, shedding light on its potential benefits and challenges. By understanding these aspects, developers can make informed decisions about whether to adopt the actor model in their projects and how to best leverage its capabilities. 

Advantages 

  • Concurrency without Locking: One of the primary advantages of the Actor Model is its ability to handle concurrent tasks effortlessly. Actors, which are independent units of execution, can communicate with each other by exchanging messages, eliminating the need for complex synchronization mechanisms. This simplifies the development process, making it easier to build highly scalable and concurrent systems, a significant advantage for zen8labs when dealing with large-scale projects. 
  • Fault Isolation and Resilience: In the Actor Model, each actor operates within its own isolated environment, entirely encapsulating its state and behavior. This enables fault isolation, ensuring that if one actor crashes or experiences a failure, it does not affect the entire system. This fault-tolerance characteristic improves system resilience, making it a valuable trait for zen8labs, where reliability and robustness are essential. 
  • Modularity and Reusability: Actors in the Actor Model are designed to be modular and self-contained, promoting code reusability. Each actor can encapsulate its own logic and state, making it easier to understand, maintain, and test. This modularity allows zen8labs developers to focus on designing individual actors independently, thereby enhancing productivity, code maintainability, and reducing development time. 
  • Scalability: The Actor Model inherently supports scalability as actors can be dynamically created, distributed, and executed across multiple physical or virtual machines. This flexibility allows zen8labs to leverage concurrent processing power and easily scale applications based on varying workload demands. With this approach, adding more actors to the system allows for seamless horizontal scaling without sacrificing performance. 

Disadvantages 

  • Learning Curve: Adopting the Actor Model may require some initial investment in learning and understanding the paradigm, especially for developers more accustomed to traditional models like object-oriented programming. It may take some time for the team to adapt to the principles, conventions, and best practices of the Actor Model. However, with proper guidance and training, this initial drawback can be overcome swiftly. 
  • Complexity in Message Passing: While message passing forms the foundation of the Actor Model, it can sometimes introduce its own set of complexities. Developers need to be careful about designing robust message-passing protocols to ensure efficient and error-free communication between actors. In complex systems, managing message sequencing, timeouts, and potential deadlocks may require additional attention and effort. 
  • Overhead: The actor model introduces a certain amount of overhead due to the message passing mechanism. Serializing and deserializing messages can impact performance, especially in highly concurrent systems. Developers must carefully optimize message passing and consider the trade-off between simplicity and performance. 

The Actor Model provides significant advantages in terms of concurrency handling, fault isolation, modularity, and scalability. By leveraging its qualities, developers can build highly concurrent, scalable, and fault-tolerant systems. Although there might be a learning curve and additional complexities involved, the benefits outweigh the disadvantages, making the Actor Model a powerful tool to tackle modern software challenges and deliver exceptional solutions. By harnessing the actor model effectively, zen8labs’ skilled developers aim to stay at the cutting edge of software development trends and provide innovative solutions for our clients. 

Let’s revisit our e-commerce site example and explore how the actor model can be utilized to implement a specific use case. In this scenario, we introduce a dedicated actor called ‘PurchaseActor’ that takes on the responsibility of sending product purchase message requests to the ‘InventoryActor.’ The ‘InventoryActor’ then handles the flow of product purchases by queuing all the requests in its mailbox. By adopting the actor model in this context, we can effectively manage and coordinate the purchase process, ensuring efficient communication and seamless handling of product transactions.

The actor model in action

The PurchaseActor communicates with the InventoryActor to purchase a product by messaging. The InventoryActor processes the message from its queue and sends a relevant message back to the ProductActor depending on the order status (successful/failed). In this scenario, the InventoryActor queues all purchase requests from multiple users/PurchaseActors in its mailbox. The PurchaseActor threads can continue with other tasks while their product purchase requests are being processed. 

Frameworks in popular backend languages enable us to implement the actor model. Examples include Akka for JVM-based languages like Java and Scala, Pykka for Python, and Gosiris for Go. 

Below is a code snippet written using the Java Akka framework, demonstrating how the product purchase flow can be implemented in a non-blocking manner. 

In the next section, we will delve into Pykka in Python and demonstrate how it effectively addresses the issue. 

Introducing Pykka in Python 

Pykka, an open-source Python library, has become zen8labs’ go-to solution for implementing the Actor model and enabling highly effective asynchronous programming. Pykka provides a clear and concise API that simplifies the creation, management, and coordination of actors within a system. 

The Effect of Pykka in Asynchronous Programming: 

  • Simplified Codebase: By adopting Pykka, we can write cleaner and more maintainable code. The Actor Model’s encapsulation of state and message passing mechanism allows for better separation of concerns, reducing complexity and making it easier to reason about the system’s behavior. 
  • Improved Responsiveness: Asynchronous programming with Pykka enables our applications to remain highly responsive even under heavy loads. By leveraging the Actor Model’s message passing, we can decouple time-consuming tasks from the main execution flow, ensuring that our applications remain responsive to user interactions. 
  • Scalability: Pykka’s support for concurrent execution empowers us to build scalable systems that can handle a large number of concurrent requests. This is particularly crucial for zen8labs as we strive to deliver software solutions that can seamlessly scale with our clients’ growing needs. 
  • Fault Tolerance: With Pykka’s supervision hierarchies, we can easily manage and recover from failures within the system. By defining a supervision strategy, we can specify how actors should handle failures and automatically restart or terminate them as needed. This ensures that our applications remain resilient and can recover from errors without compromising the overall system stability. 
  • Ease of Testing: Pykka’s actor-based approach makes it easier to test individual components of our applications in isolation. Since actors encapsulate their state and communicate through message passing, we can mock or stub the messages exchanged between actors during testing. This allows us to thoroughly test each actor’s behavior and ensure that they function correctly in isolation before integrating them into the larger system. 
  • Extensibility: Pykka’s lightweight and flexible design makes it easy to extend and customize our asynchronous applications. We can create new actors with specific behaviors and functionalities, allowing us to adapt our systems to changing requirements or add new features without disrupting the existing codebase. 

Here is an example of solving the above problem using Django and Pykka: 

from django.db import models 
from pykka import ThreadingActor 

class Product(models.Model): 
    name = models.CharField(max_length=128, unique=True) 
    in_stock = models.PositiveIntegerField(default=1) 
 
class ProductService: 
    @staticmethod 
    def get_product(product_id): 
        try: 
            return Product.objects.get(pk=product_id) 
        except Product.DoesNotExist as error: 
            raise Exception(error) 

    @staticmethod 
    def get_product_stock(product_id): 
        return ProductService.get_product(product_id).in_stock 

    @staticmethod 
    def set_in_stock(product_id, count):
        product = ProductService.get_product(product_id) 
        product.in_stock -= count 
        product.save() 

class InventoryActor(ThreadingActor): 
    def __init__(self, product_service):
        super().__init__() 
        self.product_service = product_service 
 
    def on_receive(self, message): 
        if isinstance(message, BuyProduct): 
            self.handle_buy_product(message) 

    def handle_buy_product(self, buy_product): 
        product_id = buy_product.product_id 
        count = buy_product.count 
        in_stock = self.product_service.get_product_stock(product_id) 

        if in_stock < count or in_stock == 0: 
            self.actor_ref.tell( 
                Exception( 
                    f"Insufficient stock of the product available. In stock of {product_id}: {in_stock}" 
                ) 
            ) 
        else: 
            self.product_service.set_in_stock(product_id, count) 
            self.actor_ref.tell(PurchaseSuccessful(), self.actor_ref) 

class PurchaseActor(ThreadingActor): 
    def __init__(self, inventory_actor): 
        super().__init__() 
        self.inventory_actor = inventory_actor 

    def on_receive(self, message): 
        if isinstance(message, RequestPurchase):
            self.handle_request_purchase(message) 

    def handle_request_purchase(self, request_purchase): 
        product_id = request_purchase.product_id 
        count = request_purchase.count 
        self.inventory_actor.tell(BuyProduct(product_id, count), self.actor_ref) 

class BuyProduct: 
    def __init__(self, product_id, count): 
        self.product_id = product_id 
        self.count = count 

class RequestPurchase: 
    def __init__(self, product_id, count): 
        self.product_id = product_id 
        self.count = count 

class PurchaseSuccessful: 
    pass 

product_service = ProductService() 
inventory_actor = InventoryActor.start(product_service) 
purchase_actor = PurchaseActor.start(inventory_actor) 
purchase_actor.tell(RequestPurchase(product_id=1, count=10)))

In this code, the InventoryActor and PurchaseActor classes are subclasses of pykka. ThreadingActor, which allows them to be used as Pykka actors. The on_receive method is overridden to handle the received messages. The InventoryActor handles the BuyProduct message by calling the handle_buy_product method. The PurchaseActor handles the RequestPurchase message by calling the handle_request_purchase method and sending a BuyProduct message to the InventoryActor. The BuyProduct, RequestPurchase, and PurchaseSuccessful classes are simple data classes used to pass information between actors. 

See more document about Pykka in here: https://pykka.readthedocs.io/en/stable/ 

Conclusion

In conclusion, we recognize the importance of leveraging cutting-edge technologies to build efficient and scalable software solutions. The Actor Model, with its focus on message passing and loose coupling, provides a powerful framework for concurrent programming. By adopting Pykka, a lightweight and efficient Python library that implements the Actor Model, we can take advantage of its features such as message passing, concurrency, scalability, and fault tolerance. This allows us to build highly responsive and resilient applications that can handle a large number of concurrent requests. 

***Note: 

– [1] ACID stands for Atomicity, Consistency, Isolation, and Durability, which are four important properties that need to be ensured when performing any transactional operation with a database. Specifically, these requirements pertain to the safety, reliability, and integrity of data. If a process fails to uphold these properties, it will not be able to carry out operations on the data and will be immediately terminated. 

For more interesting insights, check us out!

Tuyet Le, Software Engineer 

Related posts

In Go, slices are one of the most important data structures, providing developers with a way to work with and manage collections of data similar to what we use at zen8labs. In this blog, I will introduce some internal aspects of slices and highlight some pitfalls to avoid when using slices in Go.
5 min read
One of the key strengths of Odoo is its modular architecture, which allows developers to extend and modify existing modules to their needs. In zen8labs' latest blog, we look at some ways that you can use Odoo to your prosperity.
3 min read
For beginners exploring Redux, you'll come across many tutorials using Redux Thunk or Redux Saga to manage async actions. However, here at zen8labs we can give recommendations between using Redux Saga over Redux Thunk in large-scale projects. Read them in our latest blog.
4 min read