README.md
1# Marl
2
3Marl is a hybrid thread / fiber task scheduler written in C++ 11.
4
5## About
6
7Marl is a C++ 11 library that provides a fluent interface for running tasks across a number of threads.
8
9Marl uses a combination of fibers and threads to allow efficient execution of tasks that can block, while keeping a fixed number of hardware threads.
10
11Marl supports Windows, macOS, Linux, FreeBSD, Fuchsia, Android and iOS (arm, aarch64, mips64, ppc64 (ELFv2), x86 and x64).
12
13Marl has no dependencies on other libraries (with an exception on googletest for building the optional unit tests).
14
15Example:
16
17```cpp
18#include "marl/defer.h"
19#include "marl/event.h"
20#include "marl/scheduler.h"
21#include "marl/waitgroup.h"
22
23#include <cstdio>
24
25int main() {
26 // Create a marl scheduler using all the logical processors available to the process.
27 // Bind this scheduler to the main thread so we can call marl::schedule()
28 marl::Scheduler scheduler(marl::Scheduler::Config::allCores());
29 scheduler.bind();
30 defer(scheduler.unbind()); // Automatically unbind before returning.
31
32 constexpr int numTasks = 10;
33
34 // Create an event that is manually reset.
35 marl::Event sayHello(marl::Event::Mode::Manual);
36
37 // Create a WaitGroup with an initial count of numTasks.
38 marl::WaitGroup saidHello(numTasks);
39
40 // Schedule some tasks to run asynchronously.
41 for (int i = 0; i < numTasks; i++) {
42 // Each task will run on one of the 4 worker threads.
43 marl::schedule([=] { // All marl primitives are capture-by-value.
44 // Decrement the WaitGroup counter when the task has finished.
45 defer(saidHello.done());
46
47 printf("Task %d waiting to say hello...\n", i);
48
49 // Blocking in a task?
50 // The scheduler will find something else for this thread to do.
51 sayHello.wait();
52
53 printf("Hello from task %d!\n", i);
54 });
55 }
56
57 sayHello.signal(); // Unblock all the tasks.
58
59 saidHello.wait(); // Wait for all tasks to complete.
60
61 printf("All tasks said hello.\n");
62
63 // All tasks are guaranteed to complete before the scheduler is destructed.
64}
65```
66
67## Benchmarks
68
69Graphs of several microbenchmarks can be found [here](https://google.github.io/marl/benchmarks).
70
71## Building
72
73Marl contains many unit tests and examples that can be built using CMake.
74
75Unit tests require fetching the `googletest` external project, which can be done by typing the following in your terminal:
76
77```bash
78cd <path-to-marl>
79git submodule update --init
80```
81
82### Linux and macOS
83
84To build the unit tests and examples, type the following in your terminal:
85
86```bash
87cd <path-to-marl>
88mkdir build
89cd build
90cmake .. -DMARL_BUILD_EXAMPLES=1 -DMARL_BUILD_TESTS=1
91make
92```
93
94The resulting binaries will be found in `<path-to-marl>/build`
95
96### Windows
97
98Marl can be built using [Visual Studio 2019's CMake integration](https://docs.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=vs-2019).
99
100### Using Marl in your CMake project
101
102You can build and link Marl using `add_subdirectory()` in your project's `CMakeLists.txt` file:
103
104```cmake
105set(MARL_DIR <path-to-marl>) # example <path-to-marl>: "${CMAKE_CURRENT_SOURCE_DIR}/third_party/marl"
106add_subdirectory(${MARL_DIR})
107```
108
109This will define the `marl` library target, which you can pass to `target_link_libraries()`:
110
111```cmake
112target_link_libraries(<target> marl) # replace <target> with the name of your project's target
113```
114
115You may also wish to specify your own paths to the third party libraries used by `marl`.
116You can do this by setting any of the following variables before the call to `add_subdirectory()`:
117
118```cmake
119set(MARL_THIRD_PARTY_DIR <third-party-root-directory>) # defaults to ${MARL_DIR}/third_party
120set(MARL_GOOGLETEST_DIR <path-to-googletest>) # defaults to ${MARL_THIRD_PARTY_DIR}/googletest
121add_subdirectory(${MARL_DIR})
122```
123
124### Usage Recommendations
125
126#### Capture marl synchronization primitves by value
127
128All marl synchronization primitves aside from `marl::ConditionVariable` should be lambda-captured by **value**:
129
130```c++
131marl::Event event;
132marl::schedule([=]{ // [=] Good, [&] Bad.
133 event.signal();
134})
135```
136
137Internally, these primitives hold a shared pointer to the primitive state. By capturing by value we avoid common issues where the primitive may be destructed before the last reference is used.
138
139#### Create one instance of `marl::Scheduler`, use it for the lifetime of the process
140
141The `marl::Scheduler` constructor can be expensive as it may spawn a number of hardware threads. \
142Destructing the `marl::Scheduler` requires waiting on all tasks to complete.
143
144Multiple `marl::Scheduler`s may fight each other for hardware thread utilization.
145
146For these reasons, it is recommended to create a single `marl::Scheduler` for the lifetime of your process.
147
148For example:
149
150```c++
151int main() {
152 marl::Scheduler scheduler(marl::Scheduler::Config::allCores());
153 scheduler.bind();
154 defer(scheduler.unbind());
155
156 return do_program_stuff();
157}
158```
159
160#### Bind the scheduler to externally created threads
161
162In order to call `marl::schedule()` the scheduler must be bound to the calling thread. Failure to bind the scheduler to the thread before calling `marl::schedule()` will result in undefined behavior.
163
164`marl::Scheduler` may be simultaneously bound to any number of threads, and the scheduler can be retrieved from a bound thread with `marl::Scheduler::get()`.
165
166A typical way to pass the scheduler from one thread to another would be:
167
168```c++
169std::thread spawn_new_thread() {
170 // Grab the scheduler from the currently running thread.
171 marl::Scheduler* scheduler = marl::Scheduler::get();
172
173 // Spawn the new thread.
174 return std::thread([=] {
175 // Bind the scheduler to the new thread.
176 scheduler->bind();
177 defer(scheduler->unbind());
178
179 // You can now safely call `marl::schedule()`
180 run_thread_logic();
181 });
182}
183
184```
185
186Always remember to unbind the scheduler before terminating the thread. Forgetting to unbind will result in the `marl::Scheduler` destructor blocking indefinitely.
187
188#### Don't use externally blocking calls in marl tasks
189
190The `marl::Scheduler` internally holds a number of worker threads which will execute the scheduled tasks. If a marl task becomes blocked on a marl synchronization primitive, marl can yield from the blocked task and continue execution of other scheduled tasks.
191
192Calling a non-marl blocking function on a marl worker thread will prevent that worker thread from being able to switch to execute other tasks until the blocking function has returned. Examples of these non-marl blocking functions include: [`std::mutex::lock()`](https://en.cppreference.com/w/cpp/thread/mutex/lock), [`std::condition_variable::wait()`](https://en.cppreference.com/w/cpp/thread/condition_variable/wait), [`accept()`](http://man7.org/linux/man-pages/man2/accept.2.html).
193
194Short blocking calls are acceptable, such as a mutex lock to access a data structure. However be careful that you do not use a marl blocking call with a `std::mutex` lock held - the marl task may yield with the lock held, and block other tasks from re-locking the mutex. This sort of situation may end up with a deadlock.
195
196If you need to make a blocking call from a marl worker thread, you may wish to use [`marl::blocking_call()`](https://github.com/google/marl/blob/main/include/marl/blockingcall.h), which will spawn a new thread for performing the call, allowing the marl worker to continue processing other scheduled tasks.
197
198---
199
200Note: This is not an officially supported Google product
201