|
1 | | -# ESPressio-Threads |
2 | | -Threading Components of the Flowduino ESPressio Development Platform |
| 1 | +# ESPressio Threads |
| 2 | +Threading Components of the Flowduino ESPressio Development Platform. |
3 | 3 |
|
| 4 | +Light-weight and easy-to-use Threading for your Microcontroller development work. |
4 | 5 |
|
| 6 | +## Latest Stable Version |
| 7 | +There is currently no stable released version. |
| 8 | + |
| 9 | +## ESPressio Development Platform |
| 10 | +The **ESPressio** Development Platform is a collection of discrete (sometimes intra-connected) Component Libraries developed with a particular development ethos in mind. |
| 11 | + |
| 12 | +The key objectives of the ESPressio Development Platform are: |
| 13 | +- **Light-weight** - The Components should always strive to optimize memory consumption and operational overhead as much as possible, but not to the detriment of... |
| 14 | +- **Ease of Use** - Many of our components serve as Developer-Friendly Abstractions of existing procedural code libraries. |
| 15 | +- **Object-Oriented** - A `type` for everything, and everything in a `type`! |
| 16 | +- **SOLID**: |
| 17 | +- - > **S**ingle Responsibility Principle (SRP) |
| 18 | + Break your code into smaller, focused components. |
| 19 | +- - > **O**pen/Closed Principle (OCP) |
| 20 | + Be open for extension but closed for modification. |
| 21 | +- - > **L**iskov Substitution Principle (LSP) |
| 22 | + Be substitutable for the base type without altering correctness. |
| 23 | +- - > **I**nterface Segregation Principle (ISP) |
| 24 | + Break interfaces into specific, client-focused ones. |
| 25 | +- - > **D**ependency Inversion Principle (DIP) |
| 26 | + Be dependent on abstractions, not concretions. |
| 27 | + |
| 28 | +To the maximum extent possible within the limitations/restrictons/constraints of the C++ langauge, the Arduino platform, and Microcontroller Programming itself, all Component Libraries of the **ESPressio** Development Platform must strive to honour the **SOLID** principles. |
| 29 | + |
| 30 | +## License |
| 31 | +ESPressio (and its component libraries, including this one) are subject to the *Apache License 2.0* |
| 32 | +Please see the [](LICENSE) accompanying this library for full details. |
| 33 | + |
| 34 | +## Namespace |
| 35 | +Every type/variable/constant/etc. related to *ESPressio* Threads are located within the `Threads` submaspace of the `ESPressio` parent namespace. |
| 36 | + |
| 37 | +The namespace provides the following (*click on any declaration to navigate to more info*): |
| 38 | +- [`ESPressio::Threads::IThread`](ithread) |
| 39 | +- [`ESPressio::Threads::Thread`](thread) |
| 40 | +- [`ESPressio::Threads::Manager`](threadmanager) |
| 41 | +- [`ESPressio::Threads::GarbageCollector`](garbagecollector) |
| 42 | +- [`ESPressio::Threads::IThreadSafe`](ithreadsafe) |
| 43 | +- [`ESPressio::Threads::Mutex`](mutex) |
| 44 | +- [`ESPressio::Threads::ReadWriteMutex`](readwritemutex) |
| 45 | + |
| 46 | +## Platformio.ini |
| 47 | +You can quickly and easily add this library to your project in PlatformIO by simply including the following in your `platformio.ini` file: |
| 48 | + |
| 49 | +```ini |
| 50 | +lib_deps = |
| 51 | + https://github.com/Flowduino/ESPressio-Threads.git |
| 52 | +``` |
| 53 | + |
| 54 | +Please note that this will use the very latest commits pushed into the repository, so volatility is possible. |
| 55 | +This will of course be resolved when the first release version is tagged and published. |
| 56 | +This section of the README will be updated concurrently with each release. |
| 57 | + |
| 58 | +## Understanding Threads |
| 59 | +Threads enable us to perform concurrent and/or parallel processing on our microcontroller devices. |
| 60 | +In the case of multi-core microcontrollers, such as the ESP32, we can achieve true concurrent execution by using the components provided here in the *ESPressio* Thread Library. |
| 61 | + |
| 62 | +By default, when an instance of a [`Thread`](thread) descendant is created, presuming that you do not modify by calling `SetCoreID()` prior to initializing the instance, the Thread [`Manager`](threadmanager) will automatically allocate the Thread to the next CPU Core. |
| 63 | + |
| 64 | +For example, by default, your first Thread Instance will occupy *CPU 0*, your second will occupy *CPU 1*, your third will co-occupy *CPU 0*. |
| 65 | + |
| 66 | +However, as hinted previously (and as you'll see later in this document) you can very easily define explicitly which CPU Core you want your `Thread` to run on. |
| 67 | + |
| 68 | +Now, when your Microcontroller doesn't have multiple CPU Cores, or when you have multiple threads co-tenanting the same CPU Cores, Threads will operate on the princpals of *Time Slicing*. This is where `Thread`s are executed in *Parallel* (not the same as *Concurrent*), and they each get slices of time within which to continue execution. |
| 69 | + |
| 70 | +In this way, multiple distinct contexts can be progressed without having to wait for each of them to complete in turn. |
| 71 | + |
| 72 | +## Thread Safety |
| 73 | +Those of you familiar with multi-threading will already be aware of the need to enforce careful *thread-safety* when working with multiple `Thread`s. |
| 74 | + |
| 75 | +*ESPressio* Threads makes it easy, providing multiple choices of *Thread-Safe Locks* for you to easily use. |
| 76 | + |
| 77 | +You'll see an example later in this document. |
| 78 | + |
| 79 | +## Basic Usage |
| 80 | +ESPressio Threads have been designed with ease of use in mind. |
| 81 | + |
| 82 | +Ultimately, they are a carefully *Managed Encapsulation* of `Task`s, abstracted to operate and interface more alike a true `Thread` in modern desktop and mobile development. |
| 83 | + |
| 84 | +Let's take a look at a really simple implementation: |
| 85 | + |
| 86 | +### Includes... |
| 87 | +Before we define our `Thread`, we need to include the required header: |
| 88 | +```cpp |
| 89 | + #include <ESPressio_Thread.hpp> |
| 90 | +``` |
| 91 | + |
| 92 | +### Namespaces... |
| 93 | +Given that *ESPressio* Threads uses multi-tier Namespacing throughout, let's declare our Namespace so that we can reference the necessary type identifiers with less code: |
| 94 | +```cpp |
| 95 | + using namespace ESPressio::Threads; |
| 96 | +``` |
| 97 | +
|
| 98 | +### A `Thread` type... |
| 99 | +With the required header linked, and the namespace defined, we can now define a simple `Thread` type, which we shall call `MyFirstThread`: |
| 100 | +```cpp |
| 101 | +class MyFirstThread : public Thread { |
| 102 | + protected: |
| 103 | + void OnInitialization() override { |
| 104 | + // Anything we need to do here prior to the Thread's Loop sstarting |
| 105 | + } |
| 106 | +
|
| 107 | + void OnLoop() override { |
| 108 | + // Whatever we want to do within the Loop |
| 109 | + } |
| 110 | +}; |
| 111 | +``` |
| 112 | +>NOTE: It is not necessary to override `OnInitialization` unless you have a reason. It is virtual, not abstract. |
| 113 | + |
| 114 | +We shall be building from this basic example `class` throughout the rest of this documentation! |
| 115 | + |
| 116 | +So, the above class declaration doesn't really do anything... let's build upon it to illustrate how multiple `Thread`s work: |
| 117 | +```cpp |
| 118 | +class MyFirstThread : public Thread { |
| 119 | + private: |
| 120 | + int _counter = 0; |
| 121 | + protected: |
| 122 | + void OnInitialization() override { |
| 123 | + // Anything we need to do here prior to the Thread's Loop sstarting |
| 124 | + } |
| 125 | + |
| 126 | + void OnLoop() override { |
| 127 | + _counter++; // Increment the counter |
| 128 | + |
| 129 | + // Let's display some information about our Thread... |
| 130 | + Serial.printf("MyFirstThread::OnLoop() - Thread #%d - On CPU %d, Counter = %d", GetThreadID(), xPortGetCoreID(), _counter); |
| 131 | + |
| 132 | + delay(1000); // Let's let this Thread wait for 1 second before it loops around again |
| 133 | + } |
| 134 | +}; |
| 135 | +``` |
| 136 | +With the above changes, any instance of `MyFirstThread` will execute its `OnLoop()` method every one second, and each time it does, it'll increment a *counter*, then print out the following information in the `Serial` console: |
| 137 | +- The Thread ID |
| 138 | +- Which CPU the Thread is running on |
| 139 | +- The value of the *Counter* |
| 140 | + |
| 141 | +Admittedly, this isn't the most practical use of a `Thread`, however, it is an *illustrative* one. |
| 142 | + |
| 143 | +### The `setup()` method... |
| 144 | +Let's quickly assemble a program to use `MyFirstThread`: |
| 145 | +```cpp |
| 146 | +MyFirstThread thread1; |
| 147 | + |
| 148 | +void setup() { |
| 149 | + Serial.begin(115200); |
| 150 | + |
| 151 | + delay(500); // Small delay just so that the thread doesn't start before the Serial Monitor is ready |
| 152 | + |
| 153 | + thread1.Initialize(); |
| 154 | +} |
| 155 | +``` |
| 156 | +That's all there is to it! If you push this program to your (compatible) microcontroller, it will immediately start printing the following into your Serial console (once per second): |
| 157 | + |
| 158 | +>MyFirstThread::OnLoop() - Thread 1 - On CPU 0, Counter = 0 |
| 159 | +MyFirstThread::OnLoop() - Thread 1 - On CPU 0, Counter = 1 |
| 160 | +MyFirstThread::OnLoop() - Thread 1 - On CPU 0, Counter = 2 |
| 161 | +MyFirstThread::OnLoop() - Thread 1 - On CPU 0, Counter = 3 |
| 162 | + |
| 163 | +### What about the `loop()` method? |
| 164 | +Your existing `loop()` method will continue to operate exactly as it always has. On the ESP32, the default `loop()` method executes on CPU 1, while you will notice that your instance of `MyFirstThread` (`thread1` in the above sample code) is running on CPU 0. |
| 165 | + |
| 166 | +### The sample code so far... |
| 167 | +To make it easier to refer up and down, let's combine all of the code together now: |
| 168 | +```cpp |
| 169 | +#include <ESPressio_Thread.hpp> |
| 170 | +
|
| 171 | +using namespace ESPressio::Threads; |
| 172 | +
|
| 173 | +class MyFirstThread : public Thread { |
| 174 | + private: |
| 175 | + int _counter = 0; |
| 176 | + protected: |
| 177 | + void OnInitialization() override { |
| 178 | + // Anything we need to do here prior to the Thread's Loop sstarting |
| 179 | + } |
| 180 | + |
| 181 | + void OnLoop() override { |
| 182 | + _counter++; // Increment the counter |
| 183 | + |
| 184 | + // Let's display some information about our Thread... |
| 185 | + Serial.printf("MyFirstThread::OnLoop() - Thread #%d - On CPU %d, Counter = %d", GetThreadID(), xPortGetCoreID(), _counter); |
| 186 | + |
| 187 | + delay(1000); // Let's let this Thread wait for 1 second before it loops around again |
| 188 | + } |
| 189 | +}; |
| 190 | + |
| 191 | +MyFirstThread thread1; |
| 192 | + |
| 193 | +void setup() { |
| 194 | + Serial.begin(115200); |
| 195 | + |
| 196 | + delay(500); // Small delay just so that the thread doesn't start before the Serial Monitor is ready |
| 197 | + |
| 198 | + thread1.Initialize(); |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +### Multiple Threads? No Problem! |
| 203 | +So we've created one separate thread (ideally to execute on a separate CPU Core from the default application thread)... but what if we want more threads? |
| 204 | + |
| 205 | +That's really not a problem. |
| 206 | + |
| 207 | +Let's modify the previous example to create multiple Threads: |
| 208 | +```cpp |
| 209 | +MyFirstThread thread1, thread2, thread3; |
| 210 | + |
| 211 | +void setup() { |
| 212 | + Serial.begin(115200); |
| 213 | + |
| 214 | + delay(500); // Small delay just so that the thread doesn't start before the Serial Monitor is ready |
| 215 | + |
| 216 | + thread1.Initialize(); |
| 217 | + thread2.Initialize(); |
| 218 | + thread3.Initialize(); |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +We've now added two additional threads, so our output will look something like this: |
| 223 | +>MyFirstThread::OnLoop() - Thread 1 - On CPU 0, Counter = 1 |
| 224 | +MyFirstThread::OnLoop() - Thread 2 - On CPU 1, Counter = 1 |
| 225 | +MyFirstThread::OnLoop() - Thread 3 - On CPU 0, Counter = 1 |
| 226 | +>MyFirstThread::OnLoop() - Thread 1 - On CPU 0, Counter = 2 |
| 227 | +MyFirstThread::OnLoop() - Thread 3 - On CPU 0, Counter = 2 |
| 228 | +MyFirstThread::OnLoop() - Thread 2 - On CPU 1, Counter = 2 |
| 229 | + |
| 230 | +The explicit maximum number of *ESPresio* `Thread`s supported by the library is 256, however the *practical limit* depends entirely on the specifications of your microcontroller. It's almost certainly going to be considerably lower than 256! |
| 231 | + |
| 232 | +### The Thread Manager |
| 233 | +In the previous example, you'll see that we manually called `Initialize()` on each instance of `MyFirstThread`. |
| 234 | + |
| 235 | +Well, *ESPressio* Threads provides a central Thread `Manager`, and all of your `Thread` instances automatically register themselves with this `Manager`. |
| 236 | + |
| 237 | +This neans we can `Initialize()` all of our `Thread` Instances in a single command! |
| 238 | + |
| 239 | +First we need to make sure we include the `Thread` `Manager`'s header in our program: |
| 240 | +```cpp |
| 241 | +#include <ESPressio_ThreadManager.hpp> |
| 242 | +``` |
| 243 | +Now we can modify the previous code example accordingly: |
| 244 | +```cpp |
| 245 | +MyFirstThread thread1, thread2, thread3; |
| 246 | + |
| 247 | +void setup() { |
| 248 | + Serial.begin(115200); |
| 249 | + |
| 250 | + delay(500); // Small delay just so that the thread doesn't start before the Serial Monitor is ready |
| 251 | + |
| 252 | + Manager::Initialize(); |
| 253 | +} |
| 254 | +``` |
| 255 | +Now, all three of our `MyFirstThread` instances will start exactly as they did before, but we didn't have to explicitly `Initialize()` each of them separately. |
0 commit comments