PostSharp 4.3 / / Post­Sharp Documentation / Threading Patterns / Writing Thread-Safe Code with Threading Models / Actor Threading Model
Actor Threading Model

Given the complexity of trying to coordinate accesses to an object from several threads, sometimes it makes more sense to avoid multi threading altogether. The Actor model avoids the need for thread safety on class instances by routing method calls from each instance to a single message queue which is processed, in order, by a single thread.

Since the processing for each instance takes place in a single thread, multi-threading is avoided altogether and the object is guaranteed to be free of data races. Calls are processed asynchronously in the order in which they were added to the message queue. Because all calls to an actor are asynchronous, it is recommended that the async/await feature of C# 5.0 be used.

Additionally to provide a race-free programming model, the Actor pattern has the benefit of transparently distributing the computing load to all available CPUs without additional logic. Note that PostSharp’s implementation does not assign a new thread to each actor instance but uses a thread pool instead, so it is possible to have a very large number of actors with relatively low overhead.

This topic contains the following sections:

A single-threaded example

Consider the following example of an AverageCalculator class. The code is not thread-safe because incrementing the count has four operations (read and write) that must all be performed atomically.

C#
                        class
                         AverageCalculator
{
    
                        float
                         sum;
    
                        int
                         count;

    
                        public
                         
                        void
                         AddSample(
                        float
                         n)
    {
        
                        this
                        .count++;
        
                        this
                        .sum += n;
    }

    
                        public
                         
                        float
                         GetAverage()
    {
        
                        return
                         
                        this
                        .sum / 
                        this
                        .count;
    }
}
                      

We could use the Synchronized or Reader-Writer Synchronized threading model to make sure that the calling thread will wait if the object is currently being accessed by another thread. Another solution in this situation is to avoid concurrency altogether using the Actor pattern and asynchronous methods.

Applying the Actor model using PostSharp Tools for Visual Studio

To apply the Actor threading model to your class with PostSharp Tools for Visual Studio:

  1. Place the mouse cursor over your class name and select “Apply threading model…” from the drop-down.

    Actor 1
  2. Select “Apply actor threading model” and click Next.

    Actor 2
  3. Verify the actions on the Summary screen and click Next.

    Actor 3
  4. Click Finish after the installation completes:

    Actor 4

    Your class will now have the ActorAttribute and all dependencies will have been added to the project.

  5. Finally, you need to change all methods that have a return value to asynchronous methods, add ReentrantAttribute attribute to them, and modify the code that calls them.

Applying the Actor manually

To apply the Actor threading model manually:

  1. Add the PostSharp.Patterns.Threading NuGet package to your project.

  2. Add the ActorAttribute to the class.

  3. Finally, you need to change all methods that have a return value to asynchronous methods, and modify the code that calls them.

In the reworked example below, the AverageCalculator class has had the ActorAttribute added and the GetAverage methods has been changed into asynchronous with ReentrantAttribute attribute. The AddSample method was also changed to an async method returning Task and ReentrantAttribute attribute was applied.

C#
                        [Actor]
                        

                        class
                         AverageCalculator
{
    
                        float
                         sum;
    
                        int
                         count;

    [Reentrant]
    
                        public
                         
                        async
                         Task AddSample(
                        float
                         n)
    {
        
                        this
                        .count++;
        
                        this
                        .sum += n;
    }

    [Reentrant]
    
                        public
                         
                        async
                         Task<
                        float
                        > GetAverage()
    {
        
                        return
                         
                        this
                        .sum / 
                        this
                        .count;
    }
}
                      

You can now use the same AverageCalculator from two concurrent threads.

C#
                        class
                         Program
{
    
                        static
                         
                        void
                         Main(
                        string
                        [] args)
    {
        MainAsync().GetAwaiter().GetResult();
    }

    
                        static
                         
                        async
                         Task MainAsync()
    {
        AverageCalculator averageCalculator = 
                        new
                         AverageCalculator();

        SampleObserver observer = 
                        new
                         SampleObserver(averageCalculator);
        DataSources.Source1.Subscribe(observer);
        DataSources.Source2.Subscribe(observer);

        Console.ReadKey();

        
                        float
                         average = 
                        await
                         averageCalculator.GetAverage();

        Console.WriteLine(
                        "Average: {0}"
                        , average);
    }
}
                        


                        class
                         SampleObserver : IObserver<
                        float
                        >
{
    AverageCalculator calculator;


    
                        public
                         
                        void
                         OnNext( 
                        float
                         
                        value
                         )
    {
      
                        // Each of the data sources can call us from a different thread and concurrently.
                        

                              
                        // But we don't have to care since our calculator will enqueue method calls.
                        

                              
                        this
                        .calculator.AddSample( 
                        value
                         );
    }

    
                        // Details skipped.
                        

                        }
                      

Behind the scenes, each invocation of AverageCalculator.AddSample is added to the message queue by the ActorAttribute, which then processes each call sequentially in the order it was added to the queue. This gives us the guarantee that an instance of the AverageCalculator class is never being accessed concurrently by two threads, and eliminates the need to make take multi-threading into account.

Working with complex state

PostSharp generates code that prevents the fields of an actor class to be accessed from an invalid context. For instance, trying to read an actor field from a background task would result in a ThreadAccessException. However, very often, state is more complex than fields of simple type like int or string. State can be composed of several objects and collections.

To prevent state corruption, it is important that PostSharp generates code that enforces the Actor model at runtime even for child objects of the actor.

To add complex state to actor classes:

  1. Declare the Parent-Child relationship on the property using the ChildAttribute custom attribute:

    C#
                                  [Actor]          
                                  
    
                                  class
                                   AverageCalculator
    {
        
                                  float
                                   sum;
        
                                  int
                                   count;
    
        [Child]
        
                                  private
                                   CounterInfo counterInfo;
    
        
                                  // Other details skipped for brevity
                                  
    
                                  }
                                
  2. Add the PrivateThreadAwareAttribute attribute to the child class.

    C#
                                  [PrivateThreadAware]
                                  
    
                                  public
                                   
                                  class
                                   CounterInfo
    {
        
                                  public
                                   
                                  string
                                   Name { 
                                  get
                                  ; 
                                  set
                                  ; }
    }
                                

For more information regarding parent-child relationships in threading models, see also Parent/Child Relationships.

Dealing with constraints of the Actor model

Per definition of the Actor model, all methods are executed asynchronously. Methods that have no return value (void methods) can be executed asynchronously without syntactic changes. However, methods that do have a return value need to be made asynchronous using the async keyword.

In some situations, the application of the async keyword and the corresponding dispatching of the method may be unnecessary. For instance, a method that returns immutable information is always thread-safe and does not need to be dispatched. For more information on excluding methods from dispatching, see Opting In and Out From Thread Safety.

See Also