-
Notifications
You must be signed in to change notification settings - Fork 66
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
fosterbrereton
wants to merge
16
commits into
main
Choose a base branch
from
fosterbrereton/actor
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+678
−0
Open
stlab::actor<T> #525
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
7dac544
initial commit
fosterbrereton 26c6f4e
adding docs
fosterbrereton aab8ddc
updates
fosterbrereton abbf765
updates
fosterbrereton 025dbd3
formatting
fosterbrereton 22bc88b
Yet another build break
fosterbrereton 733d7ce
Yet another build break
fosterbrereton 9bf9539
adding `this_actor` and unique `actor_id` per actor. Also adding `exe…
fosterbrereton aa5e487
Changes from review comments by @FelixPetriconi and @sean-parent
fosterbrereton 5e3d1ad
rethrowing at the top of `entask`
fosterbrereton a78fa9a
adding thread name getter / restorer
fosterbrereton 25b279b
Yet another build break
fosterbrereton baa3e95
Yet another build break
fosterbrereton a6ad765
minor tweaks
fosterbrereton 330c64f
Adding `enqueue` and `complete` APIs, though I am not sure I like the…
fosterbrereton 8ba8974
removing docs for now
fosterbrereton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
docs/libraries/concurrency/actor.hpp/actor3CT3E/f_operator213D.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
docs/libraries/concurrency/actor.hpp/actor3CT3E/f_operator3D3D.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
``` | ||
|
||
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
31
docs/libraries/concurrency/actor.hpp/actor3CT3E/m_actor3CT3E.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)" | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
18
docs/libraries/concurrency/actor.hpp/actor3CT3E/m_set_name.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)" | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
--- |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 avoid
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:There was a problem hiding this comment.
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.)