Asynchronous systems using folly
Folly is a library by Meta (former Facebook). It provides various abstractions that are helpful when building out reliable systems.
One of the core abstractions is EventBase. It represents a thread on which all “tasks” are run. A really useful promise that EventBase makes is that tasks will be run in the same order they were placed on it. This helps reason about code execution.
To give you an idea of what EventBase is and how it is used, let’s have a look at a simple example:
1 #include <folly/io/async/EventBase.h>
2 #include <iostream>
3
4 int main() {
5 folly::EventBase evb;
6 evb.runInEventBaseThread([] { std::cout << "Hello there" << std::endl; });
7 std::thread evb_thread = std::thread([&]() { evb.loopOnce(); });
8
9 evb_thread.join();
10
11 return 0;
12 }
Here we create an EventBase (5). We schedule a function to be run on it (6). We start a new thread and “run” the EventBase. The main thread waits for the evb_thread
to complete (9) and exits.
A very common pattern is to pass an EventBase to an object (or a number of objects) and than schedule “work” on that EventBase. Work is usually scheduled in form of lambda functions. Let’s have a look at an example:
1 class X {
2 public:
3 X(folly::EventBase &evb, int val) : evb_(evb) {
4 value_ = val;
5 std::cout << "X ctor" << std::endl;
6 evb.runInEventBaseThread(
7 [&]() { std::cout << "Value is: " << value_ << std::endl; });
8 }
9
10 ~X() {
11 value_ = 0;
12 std::cout << "X dtor" << std::endl;
13 }
14
15 private:
16 int value_;
17 folly::EventBase &evb_;
18 };
19
20 int main() {
21 folly::EventBase evb;
22 { X x(evb, 3); }
23 std::thread evb_thread = std::thread([&]() { evb.loopOnce(); });
24
25 evb_thread.join();
26
27 return 0;
28 }
In this example we create an EventBase (21). Following that object X is instantiated and a reference to the EventBase is given to it. As part of instantiation of the object X we place some work on the EventBase.
Here something very interesting happens. When the task is placed onto the EventBase lambda implicitly captured `this` and `value_`. Since the object is destroyed in line 22 (that’s where x goes out of scope) when the EventBase runs our task we step into a danger zone. If you’re lucky and use sanitizers to validate programs you will get an error like this:
Otherwise you will end up with something that (probably) wasn’t expected.
What can we do to solve this issue? What should we do to schedule work correctly? One way to solve this issue is to use smart pointers. More specifically, shared pointer.
1 class X : public std::enable_shared_from_this<X> {
2 public:
3 X(folly::EventBase &evb, int val) : evb_(evb) {
4 value_ = val;
5 std::cout << "X ctor" << std::endl;
6 }
7
8 void init() {
9 evb_.runInEventBaseThread([me = shared_from_this()]() {
10 std::cout << "Value is: " << me->value_ << std::endl;
11 });
12 }
13
14 ~X() {
15 value_ = 0;
16 std::cout << "X dtor" << std::endl;
17 }
18
19 private:
20 int value_;
21 folly::EventBase &evb_;
22 };
23
24 int main() {
25 folly::EventBase evb;
26 {
27 std::shared_ptr<X> x = std::make_shared<X>(evb, 3);
28 x->init();
29 }
30 std::thread evb_thread = std::thread([&]() { evb.loopOnce(); });
31
32 evb_thread.join();
33
34 return 0;
35 }
Here, instead of using an instance of X we create a shared pointer to X. In our `init` function we schedule some “work” on the EventBase. However, notice that here we capture variable me, a shared pointer to “this” object. This way, when x goes out of scope (29), it won’t be destructed. In this example, instance managed by shared pointer x will be destroyed only after lambda function is run to completion. When you see this pattern, a shared pointer to an object that takes EventBase as a parameter, it’s most likely because some work is being scheduled on the EventBase during lifetime of that object.
Expanding on this approach, we can create a factory method in X that will ensure that only shared pointers to an instance can be created. This will help avoid an issue where an instance is created (which is not managed by shared pointer) and than “shared_from_this” is called on that instance. In C++17 and newer standards this will cause a bad_weak_ptr error but in older versions of C++ it’s an undefined behavior.