Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stlab::actor<T> #525

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/libraries/concurrency/actor.hpp/actor3CT3E/f_operator213D.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
layout: function
title: operator!=
owner: fbrereto
brief: Inequality operator
tags:
- function
defined_in_file: concurrency/actor.hpp
overloads:
bool operator!=(const actor<T> &, const actor<T> &):
arguments:
- description: __OPTIONAL__
name: x
type: const actor<T> &
- description: __OPTIONAL__
name: y
type: const actor<T> &
description: __OPTIONAL__
return: __OPTIONAL__
signature_with_names: bool operator!=(const actor<T> & x, const actor<T> & y)
namespace:
- stlab
---
23 changes: 23 additions & 0 deletions docs/libraries/concurrency/actor.hpp/actor3CT3E/f_operator3D3D.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
layout: function
title: operator==
owner: fbrereto
brief: Equality operator
tags:
- function
defined_in_file: concurrency/actor.hpp
overloads:
bool operator==(const actor<T> &, const actor<T> &):
arguments:
- description: __OPTIONAL__
name: x
type: const actor<T> &
- description: __OPTIONAL__
name: y
type: const actor<T> &
description: __OPTIONAL__
return: __OPTIONAL__
signature_with_names: bool operator==(const actor<T> & x, const actor<T> & y)
namespace:
- stlab
---
85 changes: 85 additions & 0 deletions docs/libraries/concurrency/actor.hpp/actor3CT3E/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
layout: class
title: actor<T>
owner: fbrereto
brief: Serialized, asynchronous access to a resource
tags:
- class
defined_in_file: concurrency/actor.hpp
declaration: "template <class T>\nclass stlab::actor;"
dtor: unspecified
fields:
_impl:
annotation:
- private
description: pimpl implementation instance
type: std::shared_ptr<detail::actor_instance<T>>
namespace:
- stlab
---

`actor<T>` provides asynchronous, serialized access to an instance of `T`, running on an execution context of choice. Instead of a traditional message-passing actor model implementation, `actor<T>` is given work by way of lambdas, whose results are then optionally extracted by the caller via a `stlab::future<R>`.

`actor<T>` is a lightweight alternative to a dedicated thread managing some background service for a host application. The problem with background threads is that they consume considerable resources even when they are idle. Furthermore, many background services don't need the "always on" characteristics of a thread, and would be comfortable running only when necessary.

However, `actor<T>` is not a panacea. There are several caveats to keep in mind:

1. `thread_local` variables may not retain state from task to task. Given the implementation details of the actor's executor (e.g., it may be scheduled on any number of threads in a thread pool), an actor may jump from thread to thread. Since `thread_local` variables have a per-thread affinity by definition, the variable values may change unexpectedly.
2. The thread cache penalty paid when an actor changes threads may not be suitable for high-performance/low-latency requirements. There is a cost associated with an actor jumping from one thread to another, and as in the previous case, this may happen depending on the implementation of the executor. If this cache penalty is too expensive for your use case, a dedicated worker thread may be a better fit.
3. The tasks given to an actor should not block. If the actor must wait for external input (mouse events, network/file IO, etc.) it should be fed in from outside the actor. Because the context of execution is not "owned" by the actor, it cannot presume to block the context waiting for something else to happen, or else it risks hanging (e.g., an unresponsive main thread) or deadlocking (e.g., waiting for a task that cannot complete until this task completes.)

## Example

Say we have a service, `type_rasterizer`, that we'd like to put on a background thread:

```c++
class image {
//...
};

struct type_rasterizer {
void set_text(std::string&& text);

image rasterize();

// ...
};
```

In our application, then, we will create an actor that manages an instance of this engine. By giving it the `default_executor`, the actor will run on a thread of the OS-provided thread pool (e.g., GCD on macOS/iOS).

```c++
struct my_application {
stlab::actor<type_rasterizer> _rasterizer(stlab::default_executor,
"app text rasterizer");

// ...
};
```

Then as your application is running, you can send "messages" in the form of lambdas to this actor to perform serialized, asynchronous operations. Note the first parameter of the lambda is the `type_rasterizer` itself:

```c++
void my_application::do_rasterize(std::string&& text) {
_rasterizer.send([_text = std::move(text)](type_rasterizer& rasterizer) mutable {
// This lambda will execute on the `default_executor`. Note that while in this
// lambda, the name of the thread will be the name of the actor. In this case,
// "app text rasterizer".
rasterizer.set_text(std::move(_text));
return rasterizer.rasterize();
}).then(stlab::main_executor, [](image my_rasterized_text){
draw_image_to_screen(my_rasterized_text);
}).detach();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detach is worrisome. We should discuss more in slack (and apologies, this week I'm going to be busy getting ready for C++Now so won't be responsive), but most actor systems usually create a "main actor" so you can send a result to the main actor. An actor itself is also a type of "future" so you should be able to send() to an actor and not get a future back (I don't know if you want to do that automatically for a void result, or make it a separate call). And because of this you should be able to attach an actor to a future as a continuation without generating a new future. Something like: actor. after(future, [](auto& a, auto future-result) { /* operate on actor type */ });

I would probably block on actor destruction. I detached futures - but that adds a lot of complexity (like pre-exit - and requirement that every continuation can execute unstructured) - but there should be a way to await completion of all calls to an actor so you can have:

  actor<type> a;
  //...
  co_await a.complete();
} // actor destructs here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to chat with you more on Slack about this one. I'm thinking they would do well as separate APIs from the ones that do return futures (as I have seen cases where an actor's result will want to get shuttled to another continuation lambda, possibly to another actor, but not required.)

}
```

You could also pass the argument to the lambda itself:

```c++
_rasterizer.send([](type_rasterizer& rasterizer, std::string text) {
rasterizer.set_text(std::move(text));
return rasterizer.rasterize();
}, std::move(text));
```

Note that the actor is not always running. That is, no threads are blocked on behalf of the actor while it waits for tasks to come in. Rather, the actor only schedules itself to run on its executor when it has work to do. Once the work is completed, the actor relinquishes the thread it is running on back to the executor. In this way, actors are considerably less resource-intensive than a dedicated worker thread to some background service.
31 changes: 31 additions & 0 deletions docs/libraries/concurrency/actor.hpp/actor3CT3E/m_actor3CT3E.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
layout: method
title: actor<T>
owner: fbrereto
brief: Constructor
tags:
- method
defined_in_file: concurrency/actor.hpp
is_ctor: true
overloads:
actor<T>():
annotation:
- default
description: default constructor
return: __OPTIONAL__
signature_with_names: actor<T>()
"template <class Executor, class... Args>\nactor<T>(Executor &&, std::string &&, Args &&...)":
arguments:
- description: An executor upon which this actor will run when it has tasks.
name: e
type: Executor &&
- description: Runtime name of the actor. While the actor is running, its thread will be temporarily given this name. This name can be reconfigured with a call to `set_name`.
name: name
type: std::string &&
- description: Initialization arguments for the instance of `T` the actor holds
name: args
type: Args &&...
description: executor-based constructor
return: __OPTIONAL__
signature_with_names: "template <class Executor, class... Args>\nactor<T>(Executor && e, std::string && name, Args &&... args)"
---
21 changes: 21 additions & 0 deletions docs/libraries/concurrency/actor.hpp/actor3CT3E/m_send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
layout: method
title: send
owner: fbrereto
brief: Send tasks for the actor to execute
tags:
- method
defined_in_file: concurrency/actor.hpp
overloads:
"template <typename F, typename... Args>\nauto send(F &&, Args &&...)":
arguments:
- description: __OPTIONAL__
name: f
type: F &&
- description: __OPTIONAL__
name: args
type: Args &&...
description: __OPTIONAL__
return: __OPTIONAL__
signature_with_names: "template <typename F, typename... Args>\nauto send(F && f, Args &&... args)"
---
18 changes: 18 additions & 0 deletions docs/libraries/concurrency/actor.hpp/actor3CT3E/m_set_name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
layout: method
title: set_name
owner: fbrereto
brief: Set the name of the actor
tags:
- method
defined_in_file: concurrency/actor.hpp
overloads:
auto set_name(std::string &&):
arguments:
- description: __OPTIONAL__
name: name
type: std::string &&
description: __OPTIONAL__
return: __OPTIONAL__
signature_with_names: auto set_name(std::string && name)
---
24 changes: 24 additions & 0 deletions docs/libraries/concurrency/actor.hpp/actor3CT3E/m_then.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
layout: method
title: then
owner: fbrereto
brief: Continue tasks for the actor to execute pending the completion of some future
tags:
- method
defined_in_file: concurrency/actor.hpp
overloads:
"template <typename R, typename F, typename... Args>\nauto then(stlab::future<R> &&, F &&, Args &&...)":
arguments:
- description: The future to contine
name: future
type: stlab::future<R> &&
- description: The task to run. The first argument passed to the routine will be the actor. The second argument passed will be the resolved value of the future (if it is not `void`).
name: f
type: F &&
- description: Additional arguments to pass to `f` when it is run
name: args
type: Args &&...
description: __OPTIONAL__
return: __OPTIONAL__
signature_with_names: "template <typename R, typename F, typename... Args>\nauto then(stlab::future<R> && future, F && f, Args &&... args)"
---
9 changes: 9 additions & 0 deletions docs/libraries/concurrency/actor.hpp/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
layout: library
title: stlab/actor.hpp
owner: fbrereto
brief: Header file for the `actor<T>`
tags:
- sourcefile
library-type: sourcefile
---
Loading