Hello! Welcome to the next lesson in our journey through C++ concurrency.
In the last lesson, we analyzed the Thread Pool pattern. You learned how a group of anonymous worker threads can efficiently process tasks from a shared queue, decoupling task submission from execution. This is a powerful pattern for managing stateless, parallelizable work.
Today, we'll explore a different but related pattern: the Active Object. This pattern addresses concurrency from the perspective of an object's integrity and state. The goal for this lesson is to implement the Active Object pattern to decouple method invocation from method execution for improved concurrency. Instead of a pool of threads sharing one queue, you'll see how an object can encapsulate its own dedicated thread and task queue, turning method calls into asynchronous messages.
1. The Principle: True Encapsulation for Concurrency
In standard Object-Oriented Programming, encapsulation protects an object's data from unwarranted access. However, this protection breaks down in a multithreaded environment. If multiple threads can call methods on the same object, its private data is still subject to race conditions unless you manually add locks inside every method.
The Active Object pattern provides a more robust form of encapsulation specifically for concurrency. It's built on a few key principles.
Beyond the RTOS - Part 2: Best practices of concurrent programming and Active Object pattern
This video, from Quantum Leaps, introduces the core best practices that underpin the Active Object pattern. While it mentions Real-Time Operating Systems (RTOS), the principles are universal and provide the perfect 'why' for this pattern.
Watch the segments from 02:21 to 04:58 and 07:27 to 09:41. Focus on these key ideas: Isolate Data: Avoid sharing data between threads. Asynchronous Communication: Use events or messages instead of direct calls. Message Pumps: Structure threads around an event loop. True Encapsulation: How an Active Object's private data is protected from race conditions without explicit locks in every method.
The core idea is this: you never call a method on an active object directly. Instead, you send it a message requesting an operation. The active object has its own private thread that runs an event loop, processing these messages one by one from its private queue. Since only one thread ever accesses the object's internal data, race conditions are eliminated by design.
2. The Architecture: Components of an Active Object
Let's formalize the structure. The pattern, as described in the classic "Pattern-Oriented Software Architecture" book, consists of several collaborating components.

The formal components are:
- Proxy: The public-facing interface for the client. It has the same method signatures as the real object but doesn't contain the implementation logic. Its job is to transform a method call into a
Method Requestand enqueue it. - Method Request: A command object that bundles the information needed to execute a method, including the function to be called and its arguments. In modern C++, this is elegantly implemented with
std::functionand a lambda. - Activation Queue: A thread-safe message queue owned by the active object. It stores the incoming
Method Requestobjects. TheThreadSafeQueueyou built as a Monitor Object fits this role perfectly. - Scheduler: The part of the active object that dequeues
Method Requestsfrom theActivation Queueand executes them. This is simply the main loop running in the active object's private thread. - Servant: The "real" object that owns the data and contains the actual business logic. Its methods are only ever called by the
Schedulerin the object's private thread. - Future: (Optional) A mechanism to get a return value back from a method that was invoked asynchronously.
Active Object - Schmidt & Lavender
For a formal definition of these components, we can turn to the original paper on the pattern by Schmidt and Lavender.
Read Section 6 ('Solution'), Section 7 ('Structure'), and Section 8 ('Dynamics'). Don't worry about the pre-C++11 code for now. Focus on understanding the role of each of the six participants and how they collaborate in the three phases: construction, execution, and completion.
3. The Implementation: From Theory to Modern C++
The formal description can seem complex, with many separate classes. However, modern C++ features, particularly lambdas and std::function, allow for a much more concise and elegant implementation where the Proxy, Servant, and Scheduler are all part of a single class.
Let's build one step-by-step.
3.1. A Basic Active Object with Closures
The core idea is that a public method doesn't do the work itself. Instead, it creates a lambda function (a closure) that captures the necessary arguments and contains the actual logic. This lambda is then pushed into the object's private queue.
Revisiting the Active Object Pattern - with C++11 Closures
This article from CodeProject is a fantastic guide to implementing the Active Object pattern using modern C++ features. We'll use it to build our implementation.
Read the first code block in the article (the simple C++11 implementation). Notice how the BecomeActiveObject class contains: A DispatchQueue (our Monitor Object). A std::thread (runnable) for its private execution context. A run() method that acts as the scheduler loop. Public methods (doSomething, doSomethingElse) that simply put a lambda into the queue.
The magic lies in the dispatchQueue.put([&]() { val = 2.0; }); line.
The client calls doSomethingElse(), and the call returns immediately. The actual logic (val = 2.0;) is packaged into a callable object and will be executed later by the active object's private thread. This is the decoupling of invocation from execution.
3.2. A Glimpse of the Past: The "Traditional" Approach
To appreciate the power of lambdas, it's useful to see how this was done in older C++. The "traditional" way involves creating explicit message structures, often with enums to identify the method and unions to hold parameters. The scheduler loop then becomes a large switch statement.
You don't need to study it in detail, but glance at the second major code block in the same article (LINK, section index 2) to see this C-style message-passing approach. You'll quickly see how much boilerplate and manual management is required, making the C++11 closure-based approach far superior for maintainability.
3.3. Handling Return Values with std::promise and std::future
What if a method needs to return a value? The client invokes the method and moves on. How does it get the result later? This is the exact problem std::promise and std::future are designed to solve.
The flow is as follows:
- The public proxy method creates a
std::promise. - It returns the associated
std::futureto the client immediately. - The lambda it enqueues captures the
std::promiseby reference. - Later, when the active object's thread executes the lambda, it computes the result and sets it on the promise using
promise.set_value(). - The client, holding the future, can call
.get()at any time. If the value has been set, it returns immediately. If not,.get()blocks until the promise is fulfilled.
Let's see this in code.
Revisiting the Active Object Pattern - with C++11 Closures
The same article provides an advanced implementation that demonstrates return values, parameter passing, and state access.
Carefully study the final, most advanced code example in the article. This is the key implementation for this lesson. Pay close attention to: doSomething(): How std::promise<int> and return_val.get_future().get() are used for a blocking call that returns a value. doSomethingElse(): How the this pointer is captured to safely modify the object's internal state (this->val). doSomethingWithReferenceParams(): How a promise can be used to block until a function that modifies references has completed.
Test your understanding!
In the advanced example from the article (LINK, section 4), the doSomethingWithReferenceParams method takes int &a and int &b. It uses a std::promise<void> and blocks on return_val.get_future().get(). Why is this blocking necessary? What could go wrong if the method simply enqueued the lambda and returned immediately, like doSomethingWithParams does?
Show answer
The method must block because the caller expects the referenced variables a and b to be modified when the function returns. The C++ function signature void func(int&) implies that the reference might be changed, and the caller can safely use the new value after the call.
If doSomethingWithReferenceParams returned immediately, the client thread would continue its execution. The variables a and b, which are local to the client's stack, might go out of scope or be used in subsequent calculations before the active object's thread has a chance to execute the lambda and modify them. This would lead to a data race (the client thread reading/writing a and b while the active object's thread might be writing to them) and incorrect program behavior, as the client would be using the old, unmodified values.
By blocking on the future, the proxy method upholds the contract of a reference parameter: the modification is complete before the function call returns control to the caller.
Conclusion
Today you've implemented the Active Object pattern, a powerful technique for structuring concurrent systems. You've seen how it provides true concurrency encapsulation, eliminating entire classes of data races by design.
Key Takeaways:
- Decoupling Invocation and Execution: An Active Object turns method calls into asynchronous messages, which are processed by a dedicated internal thread.
- Concurrency by Design: By serializing all access to an object's internal state through a single-threaded event loop, it avoids the need for fine-grained locking within every method.
- Components: The pattern consists of a Proxy (public API), Servant (implementation), Method Request (a lambda in C++11), an Activation Queue, and a Scheduler (the event loop).
- Modern C++ Implementation: Lambdas (
std::function) dramatically simplify the pattern, avoiding the heavy boilerplate of traditional message-passing implementations. - Asynchronous Return Values:
std::promiseandstd::futureare the essential tools for retrieving results from methods executed in another thread.
In our last lesson, you learned about a pool of stateless workers. Today, you learned about a single stateful object with its own thread. These are two fundamental building blocks for concurrent architecture.
Next Lesson Preview:
Now that you have these powerful patterns in your toolkit, the next step is to see how they can be combined to solve more complex problems. In the next lesson, we will analyze the composition of multiple patterns to solve a larger architectural problem, such as how you might use Active Objects as the tasks within a Thread Pool, or combine the Command pattern with Active Objects to build robust, undoable asynchronous systems.