-
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
base: main
Are you sure you want to change the base?
stlab::actor<T> #525
Conversation
Aren't all your caveats actually caveats about various executors and not about actors at all? |
test/actor_tests.cpp
Outdated
{ | ||
stlab::actor<int> a(stlab::default_executor, "send_then_from_void"); | ||
stlab::future<void> f0 = a.send(increment_by, 42); | ||
stlab::future<int> f1 = a.then(stlab::future<void>(f0), [](auto& x) { return x; }); |
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.
Why x is ref?
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.
Fixed.
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.
These first parameters are the actor
's task local state - in this case an int
. The lambda has exclusive access to this task state and is allowed to modify it if it so chooses. I have an example where this is done. In this case there isn't much difference between an int
and an int&
, but the actor
's task local state can be anything, and in such case the pattern to take a reference will save unnecessary copies.
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 have made the non-ref change here, but hopefully the above comment adds clarity as to why a reference here is both allowed and desired.
stlab/concurrency/actor.hpp
Outdated
/**************************************************************************************************/ | ||
|
||
struct temp_thread_name { | ||
explicit temp_thread_name(const char* name) { stlab::set_current_thread_name(name); } |
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 would not set the name of the thread. It is not worth the "trouble" from my point of view. The tasks should be short living anyway so setting the name does not make really sense. If it is just for some milliseconds who will observer the thread name?
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.
This is very helpful for debugging, as often you will break in a random thread (managed by the thread pool) and you will want to know specifically which actor you are dealing with (especially when there are many actor<T>
instances floating around for the same T
). I suppose a workaround would be to have some kind of identifier in the T
of every actor, but that seemed like a less useful pattern than letting the actor name the thread while it is using it.
return rasterizer.rasterize(); | ||
}).then(stlab::main_executor, [](image my_rasterized_text){ | ||
draw_image_to_screen(my_rasterized_text); | ||
}).detach(); |
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 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.
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.)
stlab/concurrency/actor.hpp
Outdated
_this->_instance._x = T(std::forward<Args>(args)...); | ||
}, | ||
std::forward<Args>(args)...) | ||
.detach(); |
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.
Concerned by any call to detach()
.
…cutor`/`entask` and removing `send`/`then`
… implementation.
Yes, that's a great point. They have been brought up in the past talking about actors, though, so I thought they bore mentioning here if someone new to actors/executors is unaware of them. |
actor<T>
provides asynchronous, serialized access to an instance ofT
, 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 astlab::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: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. Sincethread_local
variables have a per-thread affinity by definition, the variable values may change unexpectedly.(@dabrahams rightly observes that these issues are caveats about executors more than they are to actors.)
Example
Say we have a service,
type_rasterizer
, that we'd like to put on a background thread: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).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:You could also pass the argument to the lambda itself:
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.