You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -4,7 +4,7 @@ Threading Components of the Flowduino ESPressio Development Platform.
4
4
Light-weight and easy-to-use Threading for your Microcontroller development work.
5
5
6
6
## Latest Stable Version
7
-
There is currently no stable released version.
7
+
The latest Stable Version is [1.0.0](https://github.com/Flowduino/ESPressio-Threads/releases/tag/1.0.0).
8
8
9
9
## ESPressio Development Platform
10
10
The **ESPressio** Development Platform is a collection of discrete (sometimes intra-connected) Component Libraries developed with a particular development ethos in mind.
@@ -46,14 +46,18 @@ The namespace provides the following (*click on any declaration to navigate to m
46
46
## Platformio.ini
47
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
48
49
+
```ini
50
+
lib_deps =
51
+
flowduino/ESPressio-Threads@^1.0.0
52
+
```
53
+
54
+
Alternatively, if you want to use the bleeding-edge (effectively "Developer Integration Testing" or "DIT") sources, you can instead use:
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
61
58
62
## Understanding Threads
59
63
Threads enable us to perform concurrent and/or parallel processing on our microcontroller devices.
@@ -206,7 +210,9 @@ That's really not a problem.
206
210
207
211
Let's modify the previous example to create multiple Threads:
208
212
```cpp
209
-
MyFirstThread thread1, thread2, thread3;
213
+
MyFirstThread thread1;
214
+
MyFirstThread thread2;
215
+
MyFirstThread thread3;
210
216
211
217
voidsetup() {
212
218
Serial.begin(115200);
@@ -236,15 +242,17 @@ In the previous example, you'll see that we manually called `Initialize()` on ea
236
242
237
243
Well, *ESPressio* Threads provides a central Thread `Manager`, and all of your `Thread` instances automatically register themselves with this `Manager`.
238
244
239
-
This neans we can `Initialize()` all of our `Thread` Instances in a single command!
245
+
This means we can `Initialize()` all of our `Thread` Instances in a single command!
240
246
241
-
First we need to make sure we include the `Thread``Manager`'s header in our program:
247
+
First we need to make sure we include the `ThreadManager`'s header in our program:
242
248
```cpp
243
249
#include<ESPressio_ThreadManager.hpp>
244
250
```
245
251
Now we can modify the previous code example accordingly:
246
252
```cpp
247
-
MyFirstThread thread1, thread2, thread3;
253
+
MyFirstThread thread1;
254
+
MyFirstThread thread2;
255
+
MyFirstThread thread3;
248
256
249
257
voidsetup() {
250
258
Serial.begin(115200);
@@ -293,7 +301,9 @@ I've also added a public *Constructor* to expose the overloaded constructor on `
293
301
294
302
Let's modify the way we define the *Instances* of `MyFirstThread` so that we can leverage Automatic Garbage Collection:
295
303
```cpp
296
-
MyFirstThread* thread1, thread2, thread3;
304
+
MyFirstThread* thread1;
305
+
MyFirstThread* thread2;
306
+
MyFirstThread* thread3;
297
307
298
308
void setup() {
299
309
Serial.begin(115200);
@@ -314,4 +324,164 @@ At that moment, the *Automatic Garbage Collector* will be awoken, and will take
314
324
315
325
>It is important to understand when it's appropriate to take advantage of Automatic Garbage Collection, and when you should manually manage the memory of your `Thread`s.
316
326
317
-
It's also good to know that the *Automatic Garbage Collector* is a "good citizen" and doesn't take up undue memory or clock cycles when it doesn't have any garbage to collect.
327
+
It's also good to know that the *Automatic Garbage Collector* is a "good citizen" and doesn't take up undue memory or clock cycles when it doesn't have any garbage to collect.
328
+
329
+
## Thread-Safe Members (Properties)
330
+
When working with multiple Threads (*especially on multi-core hardware such as the ESP32 microcontrollers*) it is absolutely critical that we identify any and all *members* (properties) within our Objects that may be simultainously accessed (be that read or write) by multiple Threads at any given moment.
331
+
332
+
Single Byte Types (such as `bool`, `byte`, and `uint8_t`... just a few examples) are generally considered **Atomic**, meaning that modifying their value occurs in a single cycle, and therefore they are considered to be inherently Thread-Safe Types.
333
+
334
+
However, most Types are more than a single Byte, and these are never inherently Thread-Safe.
335
+
336
+
For that reason, *ESPressio Threads* provides a neat "decorator" which can be used for Object Members (properties) whose values may be read and modified by multiple threads at any given moment.
337
+
338
+
Let's provide a simple illustrative example of an *unsafe* member in a Thread:
339
+
```cpp
340
+
classNotThreadSafeThread : publicThread {
341
+
private:
342
+
int _counter = 0;
343
+
protected:
344
+
void OnInitialization() override {
345
+
// Anything we need to do here prior to the Thread's Loop sstarting
346
+
}
347
+
348
+
void OnLoop() override {
349
+
_counter++; // Increment the counter
350
+
351
+
// Let's display some information about our Thread...
352
+
Serial.printf("MyFirstThread::OnLoop() - Thread #%d - On CPU %d, Counter = %d", GetThreadID(), xPortGetCoreID(), _counter);
353
+
354
+
if (_counter == 10) {
355
+
Termiante(); // This will Terminate the Thread
356
+
}
357
+
358
+
delay(1000); // Let's let this Thread wait for 1 second before it loops around again
The above example is **NOT** Thread-Safe, because the member `_counter` is *publicly exposed* through the methods `GetCounter` and `SetCounter`, which may be invoked by *other Threads* any number of times, potentially concurrently.
370
+
371
+
This means that, should one Thread invoke `SetCounter` at the same moment another thread invokes either `SetCounter` or `GetCounter`, unpredictable and undefined behaviour can occur (which can in fact crash your program entirely).
372
+
373
+
At the same time, the `OnLoop` method above is also incrementing `_counter`, and if this occurs at the same instant that another Thread invokes `GetCounter` or `SetCounter`, we can end up in an undefined state where the program is likely to crash.
374
+
375
+
So, how can we quickly and easily make `NotThreadSafeThread` into `ThreadSafeThread`?
376
+
377
+
Well, beyond just changing the name, let's take a look:
378
+
```cpp
379
+
#include <ESPressio_ThreadSafe.hpp> // < This provides access to our Thread Safe Types
380
+
381
+
class ThreadSafeThread : public Thread {
382
+
private:
383
+
IThreadSafe<int>* _counter_ = new ReadWriteMutex<cint>(0);
384
+
protected:
385
+
void OnInitialization() override {
386
+
// Anything we need to do here prior to the Thread's Loop sstarting
387
+
}
388
+
389
+
void OnLoop() override {
390
+
int counter = 0;
391
+
_counter_->WithWriteLock([&](int& value) {
392
+
value++;
393
+
counter = value; // Set the local copy so we can use it without locking the member again
394
+
});
395
+
396
+
// Let's display some information about our Thread...
397
+
Serial.printf("MyFirstThread::OnLoop() - Thread #%d - On CPU %d, Counter = %d", GetThreadID(), xPortGetCoreID(), counter);
398
+
399
+
if (_counter == 10) {
400
+
Termiante(); // This will Terminate the Thread
401
+
}
402
+
403
+
delay(1000); // Let's let this Thread wait for 1 second before it loops around again
The above code shows how we can leverage the ESPressio Threads `ReadWriteMutex` type to encapsulate a member value (in this case, `_counter` of the `int` type) so that we can access it for both Read and Write in a Thread-Safe way.
418
+
419
+
>Note that `ReadWriteMutex` operates on the principle of **Multi-Read, Exclusive-Write**, which makes the most sense in this example context. You can identally use the `Mutex` type (provided inside the `ESPressio_ThreadSafe.hpp` header file also) if you want *Exclusive-Read, Exclusive-Write* behaviour.
420
+
421
+
Let's unpick this code to see what each piece is doing.
422
+
423
+
We'll start with the member (property) declaration of `_counter` itself.
424
+
425
+
```cpp
426
+
IThreadSafe<int>* _counter_ = new ReadWriteMutex<cint>(0);
427
+
```
428
+
This declares `_counter` to be of the type `ReadWriteMutex<int>` (our `ReadWriteMutex` type-specialized for `int`).
429
+
Additionally, it creates a new *instance* of this `ReadWriteMutex`, and the constructor takes the *intiial value* (`0` in this case) for our member.
430
+
431
+
Please take special note that `_counter` is a **pointer** to a `ReadWriteMutex<int>`. This is necessary due to linguistic behaviour in C++.
432
+
433
+
Indeed, because we declare the member to be a **pointer**, we must use the `->` accessor for its members and methods, rather than a `.`.
434
+
435
+
Additionally, because it is a **pointer**, we require the destructor...
436
+
```cpp
437
+
~ThreadSafeThread() {
438
+
delete _counter; // We need to clean up the memory here
439
+
}
440
+
```
441
+
... which ensures that the instance of the `ReadWriteMutex<int>` is destroyed when its owning `ThreadSafeThread` is destroyed. Failure to implement the destructor will result in *Memory Leaks*, so please remember to manage your memory like this properly.
442
+
443
+
Next, let's take a look at the `Loop()` implementation's use of `_counter`:
444
+
```cpp
445
+
int counter = 0;
446
+
_counter_->WithWriteLock([&](int& value) {
447
+
value++;
448
+
counter = value; // Set the local copy so we can use it without locking the member again
449
+
});
450
+
```
451
+
The first line initializes a "local copy" variable, initially set with a value of `0`, that will be updated to contain the current (incremented) value of the counter.
452
+
453
+
`WithWriteLock` then defines a *Lambda Function* (the remainder of the code in the above snip) that is excuted only after the Thread-Safe Lock (the `ReadWriteMutex`) has been safely acquired.
454
+
455
+
Everything occuring inside the *Lambda Function* occurs with the **Exclusive Write** lock engaged, meaning it is 100% thread-safe for the duration of execution.
456
+
457
+
We then increment the `value` (which is a *Reference* to the actual `int` value itself).
458
+
459
+
Finally, we update our local variable `counter` to contain the current `value` (which was just incremented).
460
+
461
+
The moment that *Lambda Function* returns, the **Exclusive Write** lock is released, meaning that any other Thread can now acquire it as desired.
462
+
463
+
Now let's take a look at the Getter and Setter for `Counter`:
You will notice that each of these respective methods invokes `Get()` or `Set()` respectively.
470
+
These methods (which belong to `ReadWriteMutex` and are concretions of the common `IThreadSafe` interface) take responsibility for acquiring and releasing the appropriate Thread-Safe Lock, making these methods entirely Thread-Safe.
471
+
472
+
This means that all `public`, `protected`, and `private` methods having access to `_counter` are performing every operation (reads and writes) within the protection of the Thread-Safe Lock.
473
+
474
+
### Additional Methods of `IThreadSafe`
475
+
As well as those we have seen above (`Get()`, `Set()`, and `WithWriteLock`), the `IThreadSafe` interface (and all concrete implementations thereof, such as `ReadWriteMutex` and `Mutex`) provide a number of other Methods that will be useful to you depending on your use-cases.
476
+
477
+
Let's take a look at them now in brief:
478
+
*`TryGet(T defaultValue)` will attempt to obtain the Thread-Safe (Read) Lock, and if it is available in that instant, will return the actual value. If the Thread-Safe (Read) Lock is *not* available in that instant, the value given for parameter `defaultValue` will instead be returned.
479
+
This may be useful if it is not absolutely critical to get the precise (actual) value of a member at the point of request.
480
+
*`TrySet(T value)` will attempt to obtain the Thread-Safe (Write) Lock, and if it is available in that instant, will set the value to that given for `value`. This method returns a `bool`, where `true` indicates that the value was updated, and `false` indicates that it was *not* updated.
481
+
*`IsLockedRead()` returns a bool where `true` indicates that the Thread-Safe (Read) Lock is currently unavailable, while `false` indicates that it is current available. **IMPORTANT** If `false` is returned, then your invoking Thread will have acquired the Lock, and you will need to invoke `ReleaseLock()` to relinquish the Lock (explicitly).
482
+
*`IsLockedWrite()` returns a bool where `true` indicates that the Thread-Safe (Write) Lock is currently unavailable, while `false` indicates that it is current available.
483
+
*`WithReadLock()` invokes the given *Lambda Function* (which takes a reference to `T` - the specialized value type - as its parameter) and executes that *Lambda Function* within the protection of the acquired Thread-Safe (Read) lock.
484
+
*`WithWriteLock()` invokes the given *Lambda Function* (which takes a reference to `T` - the specialized value type - as its parameter) and executes that *Lambda Function* within the protection of the acquired Thread-Safe (Write) lock.
485
+
*`TryWithReadLock()` operates almost identically to `WithReadLock()`, however it will *only* execute the *Lambda Function* if the Thread-Safe (Read) Lock is immediately available at the instant of request. It returns a `bool` where `true` indicates that the Lock was acquired (therefore the *Lambda Function* executed) and `false` indicates that the Lock was unavailable (therefore the *Lamdba Function* was *not* executed).
486
+
*`TryWithWriteLock()` operates almost identically to `WithWriteLock()`, however it will *only* execute the *Lambda Function* if the Thread-Safe (Write) Lock is immediately available at the instant of request. It returns a `bool` where `true` indicates that the Lock was acquired (therefore the *Lambda Function* executed) and `false` indicates that the Lock was unavailable (therefore the *Lamdba Function* was *not* executed).
487
+
*`ReleaseLock()` explicitly releases the Thread-Safe Lock. This **must** be called if you previously called `IsLocked()` and it returned `false`.
sentence=Threding Library intended for use with multi-core ESP microcontrollers
6
6
paragraph=This library is intended to be used with the ESP32 and ESP32-S2 microcontrollers. It is designed to allow the user to very easily create and manage threads on the microcontroller. This library is intended to be used with PlatformIO and the Arduino framework.
0 commit comments