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