-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[core] Use an actor model for tile worker concurrency #6337
Conversation
@jfirebaugh, thanks for your PR! By analyzing this pull request, we identified @ansis, @tmpsantos and @mikemorris to be potential reviewers. |
54e81ca
to
13f7484
Compare
\o/ |
96b49ca
to
5468f86
Compare
Added unit tests and did some high-intensity panning, zooming, rotating, and style switching on my iPhone. Everything is looking good. @tmpsantos, @kkaefer, want to review? |
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.
Good job with this refactoring.
void ThreadPool::schedule(std::weak_ptr<Mailbox> mailbox) { | ||
std::lock_guard<std::mutex> lock(mutex); | ||
queue.push(mailbox); | ||
cv.notify_one(); |
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.
Is this safe? If all threads are busy will this cause the message on the mailbox to starve because no thread will be waiting on the synchronization point?
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.
It's safe because we use the version of condition_variable::wait
which accepts a predicate. If all threads are busy, on the subsequent loop whichever thread obtains the mutex first will check the predicate !queue.empty() || terminate.load()
, see that it's true, and not wait on the condition variable.
queue.pop(); | ||
lock.unlock(); | ||
|
||
if (auto locked = mailbox.lock()) { |
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 particularly find using a = b
inside an if
confusing as I tend to think it was a typo.
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 the idiomatic use of weak_ptr
; see e.g. here. In general, declaring on a separate line would require an extra set of braces and indentation to preserve the same region of locking:
{
auto locked = mailbox.lock();
if (locked) {
...
}
}
@@ -99,6 +101,7 @@ Map::Impl::Impl(View& view_, | |||
contextMode(contextMode_), | |||
pixelRatio(view.getPixelRatio()), | |||
asyncUpdate([this] { update(); }), | |||
workerThreadPool(4), |
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.
Maybe we should move the number of works to constants.hpp
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.
Agreed. I think we can target that for a followup that also makes the thread pool global, instead of per-Map
.
@@ -78,6 +81,14 @@ class RunLoop : private util::noncopyable { | |||
|
|||
void push(std::shared_ptr<WorkTask>); | |||
|
|||
void schedule(std::weak_ptr<Mailbox> mailbox) override { |
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.
Nice, so when we get rid of util::Thread
we can get rid of all the locking and complexity of the RunLoop
, I suppose.
class Mailbox; | ||
|
||
/* | ||
A `Scheduler` is responsible for coordinating the processing of messages by |
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 love this abstraction. It could be used for creating a out-of-process worker if ever needed.
|
||
namespace mbgl { | ||
|
||
RasterTileWorker::RasterTileWorker(ActorRef<RasterTileWorker>, ActorRef<RasterTile> parent_) |
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.
It is not clear to me what is the first parameter on this constructor used for.
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.
Good point, I'll document this.
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.
@kkaefer can you also 👀 before we merge?
a069a0e
to
d8f9489
Compare
d8f9489
to
d2246b2
Compare
GeometryTile
andRasterTile
now communicate with their asynchronous worker according to the actor model.This has the following benefits:
GeometryTileWorker
andRasterTileWorker
).Worker
wrappers for everyTileWorker
method.Code docs
An
Actor<O>
is an owning reference to an asynchronous object of typeO
: an "actor".Communication with an actor happens via message passing: you send a message to the object
(using
invoke
), passing a pointer to the member function to call and arguments whichare then forwarded to the actor.
The actor receives messages sent to it asynchronously, in a manner defined its
Scheduler
.To store incoming messages before their receipt, each actor has a
Mailbox
, which acts asa FIFO queue. Messages sent from actor S to actor R are guaranteed to be processed in the
order sent. However, relative order of messages sent by two different actors S1 and S2
to R is not guaranteed (and can't be: S1 and S2 may be acting asynchronously with respect
to each other).
Construction and destruction of an actor is currently synchronous: the corresponding
O
object is constructed synchronously by the
Actor
constructor, and destructed synchronouslyby the
~Actor
destructor, after ensuring that theO
is not currently receiving anasynchronous message. (Construction and destruction may change to be asynchronous in the
future.)
An
Actor<O>
can be converted to anActorRef<O>
, a non-owning value object representinga (weak) reference to the actor. Messages can be sent via the
Ref
as well.It's safe -- and encouraged -- to pass
Ref
s between actors via messages. This is how two-waycommunication and other forms of collaboration between multiple actors is accomplished.
It's safe for a
Ref
to outlive itsActor
-- the reference is "weak", and does not extendthe lifetime of the owning Actor, and sending a message to a
Ref
whoseActor
has died isa no-op. (In the future, a dead-letters queue or log may be implemented.)
Please don't send messages that contain shared pointers or references. That subverts the
purpose of the actor model: prohibiting direct concurrent access to shared state.
A
Scheduler
is responsible for coordinating the processing of messages byone or more actors via their mailboxes. It's an abstract interface. Currently,
the following concrete implementations exist:
ThreadPool
can coordinate an unlimited number of actors over any number ofthreads via a pool, preserving the following behaviors:
concurrency within a mailbox
Subject to these constraints, processing can happen on whatever thread in the
pool is available.
RunLoop
is aScheduler
that is typically used to create a mailbox andActorRef
for an object that lives on the main thread and is not itself wrappedas an
Actor
:Notes on implementation patterns
The actor model is a constrained environment. All you can do is send messages! Implementing behaviors that would be easy in a single threaded or shared memory environment takes a degree of cleverness, or at least knowledge of certain patterns.
Here are the patterns that
GeometryTile
/GeometryTileWorker
use to get their job done:GeometryTile
to know when the worker has finished processing the most recent data it's been sent, such that the tile can be considered "fully loaded".GeometryTileWorker
is a State Machine: it transitions between states depending on which messages it receives, and its behavior for messages of a certain type depends on what state it's in.GeometryTileWorker
creates a Self-Sent Message: a message it sends to itself, asynchronously. Its state transitions are defined in such a way that this allows it to coalesce the other messages it receives, to avoid doing obsolete work.If we use actors more, which I think we should, we'll probably reuse these patterns and more.
TODO
Now:
Future:
Thread
users to actors, and removeThread
. This is a strictly better model.