Skip to content

Commit 56a1e94

Browse files
committed
- Prepared Release 1.0.0
1 parent 6384477 commit 56a1e94

File tree

4 files changed

+190
-20
lines changed

4 files changed

+190
-20
lines changed

README.md

Lines changed: 180 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Threading Components of the Flowduino ESPressio Development Platform.
44
Light-weight and easy-to-use Threading for your Microcontroller development work.
55

66
## 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).
88

99
## ESPressio Development Platform
1010
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
4646
## Platformio.ini
4747
You can quickly and easily add this library to your project in PlatformIO by simply including the following in your `platformio.ini` file:
4848

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:
55+
4956
```ini
5057
lib_deps =
5158
https://github.com/Flowduino/ESPressio-Threads.git
5259
```
53-
5460
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.
5761

5862
## Understanding Threads
5963
Threads enable us to perform concurrent and/or parallel processing on our microcontroller devices.
@@ -206,7 +210,9 @@ That's really not a problem.
206210

207211
Let's modify the previous example to create multiple Threads:
208212
```cpp
209-
MyFirstThread thread1, thread2, thread3;
213+
MyFirstThread thread1;
214+
MyFirstThread thread2;
215+
MyFirstThread thread3;
210216

211217
void setup() {
212218
Serial.begin(115200);
@@ -236,15 +242,17 @@ In the previous example, you'll see that we manually called `Initialize()` on ea
236242

237243
Well, *ESPressio* Threads provides a central Thread `Manager`, and all of your `Thread` instances automatically register themselves with this `Manager`.
238244

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!
240246

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:
242248
```cpp
243249
#include <ESPressio_ThreadManager.hpp>
244250
```
245251
Now we can modify the previous code example accordingly:
246252
```cpp
247-
MyFirstThread thread1, thread2, thread3;
253+
MyFirstThread thread1;
254+
MyFirstThread thread2;
255+
MyFirstThread thread3;
248256

249257
void setup() {
250258
Serial.begin(115200);
@@ -293,7 +301,9 @@ I've also added a public *Constructor* to expose the overloaded constructor on `
293301
294302
Let's modify the way we define the *Instances* of `MyFirstThread` so that we can leverage Automatic Garbage Collection:
295303
```cpp
296-
MyFirstThread* thread1, thread2, thread3;
304+
MyFirstThread* thread1;
305+
MyFirstThread* thread2;
306+
MyFirstThread* thread3;
297307
298308
void setup() {
299309
Serial.begin(115200);
@@ -314,4 +324,164 @@ At that moment, the *Automatic Garbage Collector* will be awoken, and will take
314324

315325
>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.
316326
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+
class NotThreadSafeThread : public Thread {
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
359+
}
360+
public:
361+
NotThreadSafeThread(bool freeOnTerminate) : Thread(freeOnTerminate) {}
362+
363+
int GetCounter ( return _counter; )
364+
365+
void SetCounter(int counter) { _counter = counter; }
366+
};
367+
```
368+
369+
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
404+
}
405+
public:
406+
ThreadSafeThread(bool freeOnTerminate) : Thread(freeOnTerminate) {}
407+
408+
~ThreadSafeThread() {
409+
delete _counter; // We need to clean up the memory here
410+
}
411+
412+
int GetCounter ( return _counter->Get(); )
413+
414+
void SetCounter(int counter) { _counter->Set(counter); }
415+
};
416+
```
417+
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`:
464+
```cpp
465+
int GetCounter ( return _counter->Get(); )
466+
467+
void SetCounter(int counter) { _counter->Set(counter); }
468+
```
469+
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`.

component.mk

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
COMPONENT_ADD_INCLUDEDIRS := include
22
COMPONENT_SRCDIRS := src
33
CXXFLAGS += -DESPRESSIO_THREADS
4-
CXXFLAGS += -DESPRESSIO_THREADS_VERSION_MAJOR=0
4+
CXXFLAGS += -DESPRESSIO_THREADS_VERSION_MAJOR=1
55
CXXFLAGS += -DESPRESSIO_THREADS_VERSION_MINOR=0
6-
CXXFLAGS += -DESPRESSIO_THREADS_VERSION_PATCH=1
7-
CXXFLAGS += -DESPRESSIO_THREADS_VERSION_STRING=\"0.0.1\"
6+
CXXFLAGS += -DESPRESSIO_THREADS_VERSION_PATCH=0
7+
CXXFLAGS += -DESPRESSIO_THREADS_VERSION_STRING=\"1.0.0\"

library.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "Flowduino ESPressio-Threads",
2+
"name": "ESPressio-Threads",
33
"description": "Threding Library intended for use with multi-core ESP microcontrollers",
44
"keywords": "thread,threads,threading,esp,esp32,esp8266,espressif,multi-threading,multi-core",
55
"authors": {
@@ -10,10 +10,10 @@
1010
"type": "git",
1111
"url": "https://github.com/Flowduino/ESPressio-Threads.git"
1212
},
13-
"version": "0.0.1",
13+
"version": "1.0.0",
1414
"license": "Apache-2.0",
15-
"frameworks": "arduino",
16-
"platforms": "espressif32,espressif8266",
15+
"frameworks": "*",
16+
"platforms": "*",
1717
"dependencies": [
1818
]
1919
}

library.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
name=Flowduino ESPressio-Threads
2-
version=0.0.1
1+
name=ESPressio-Threads
2+
version=1.0.0
33
author=Simon J. Stuart
44
maintainer=Flowduino.com
55
sentence=Threding Library intended for use with multi-core ESP microcontrollers
66
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.
77
category=Data Processing
88
url=https://github.com/Flowduino/ESPressio-Threads
9-
architectures=esp32, esp32s2
9+
architectures=*

0 commit comments

Comments
 (0)