Skip to content

Commit 97e5058

Browse files
author
dave
committed
#27 Yield for micros could get stuck because of running task. #25 Locking made to work within bus in sync and async modes
1 parent b4a9baa commit 97e5058

File tree

5 files changed

+161
-66
lines changed

5 files changed

+161
-66
lines changed
Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11

22
#include <TaskManagerIO.h>
3-
#include <ReentrantYieldingLock.h>
3+
#include <SimpleSpinLock.h>
44

55
// this is the global lock that that we'll use to protect the variable below myVar.
6-
ReentrantYieldingLock myLock;
6+
SimpleSpinLock myLock;
77
volatile int myVar = 0;
8+
int localVarCopy = 0;
89

910
//
1011
// A simple log function for writing to serial
@@ -22,45 +23,71 @@ void log(const char* str, int val) {
2223
// functionality is working
2324
//
2425
void nestedFunction() {
26+
// here we take a nested lock, which is allowed by our lock, which is essentially reference counted for up to
27+
// 255 levels of depth.
2528
TaskMgrLock locker(myLock);
2629
log("in nested function", myVar);
2730
}
2831

32+
void stateMachineLockingAsync();
33+
2934
void setup() {
3035
Serial.begin(115200);
3136

3237
// start a task that locks the bus, calls a nested function and yields time back to task manager.
33-
taskManager.scheduleFixedRate(1000, [] {
34-
TaskMgrLock locker(myLock);
35-
int myVarAtStart = myVar;
36-
log("start task function", myVar);
37-
38-
// the nested function will lock again, which is fine because it's in the same task
39-
nestedFunction();
38+
taskManager.scheduleFixedRate(1000, stateMachineLockingAsync);
4039

41-
// now we release task manager to run other tasks, the will not be able to take our lock
42-
taskManager.yieldForMicros(millisToMicros(500));
4340

44-
// now we have the context back call the nested lock again
45-
nestedFunction();
41+
taskManager.scheduleFixedRate(250, [] {
42+
log("Running inner task, before lock ", myLock.getLockCount());
4643

47-
if(myVar != myVarAtStart) {
48-
log("ERROR in locking ", myVarAtStart);
49-
}
44+
// take the lock
45+
TaskMgrLock locker(myLock);
5046

51-
log("exit task function", myVar);
52-
});
47+
log("locked synchronously, count", myLock.getLockCount());
5348

54-
taskManager.scheduleFixedRate(5, [] {
55-
// here we have another task that locks, it will need to acquire the lock exclusively before entering
56-
TaskMgrLock locker(myLock);
5749
// do something that's unlikely to be optimised out, to waste some time
5850
for(int i=0; i < 100; i++) {
5951
myVar++;
6052
}
53+
54+
log("Synchronous task done", myVar);
55+
56+
// lock released at end of function
6157
});
6258
}
6359

6460
void loop() {
6561
taskManager.runLoop();
66-
}
62+
}
63+
64+
//
65+
// This function shows how to use the lock asynchronously, in code where the lock being taken and unlocked are not
66+
// sequential actions. Great care is needed with this approach to avoid deadlocks. It is far easier to get the regular
67+
// TaskMgrLock based locking right.
68+
//
69+
void stateMachineLockingAsync() {
70+
log("Before try lock, count", myLock.getLockCount());
71+
if(myLock.tryLock()) {
72+
// do a check to ensure nothing else has run.
73+
if(localVarCopy != myVar) {
74+
log("ERROR - another task got into the lock", myVar - localVarCopy);
75+
}
76+
77+
log("We already had the async lock, now unlock, count", myLock.getLockCount());
78+
myLock.unlock();
79+
}
80+
else {
81+
// we didn't have the lock, try and acquire with a spin wait, that times out if it takes too long.
82+
log("We need to do async lock", myLock.getLockCount());
83+
84+
bool gotLock = myLock.spinLock(millisToMicros(10));
85+
if(gotLock) {
86+
localVarCopy = myVar;
87+
log(" - got the lock", myLock.getLockCount());
88+
}
89+
else {
90+
log(" - spin wait failed", myLock.getLockCount());
91+
}
92+
}
93+
}

src/ReentrantYieldingLock.cpp renamed to src/SimpleSpinLock.cpp

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,45 @@
33
* This product is licensed under an Apache license, see the LICENSE file in the top-level directory.
44
*/
55

6-
#include "ReentrantYieldingLock.h"
6+
#include "SimpleSpinLock.h"
77

8-
bool ReentrantYieldingLock::spinLock(unsigned long micros) {
8+
bool SimpleSpinLock::tryLock() {
99
// if our task already owns the lock then we are good.
10-
if(locked && initiatingTask && taskManager.getRunningTask() == initiatingTask) return true;
10+
return (locked && initiatingTask && taskManager.getRunningTask() == initiatingTask);
11+
}
12+
13+
bool SimpleSpinLock::spinLock(unsigned long iterations) {
14+
if(tryLock()) {
15+
++count;
16+
return true;
17+
}
1118

1219
// otherwise we contend to get the lock in a spin wait until we exhaust the micros provided.
13-
bool areWeLocked = false;
14-
while(!locked && micros > 50) {
15-
areWeLocked = tm_internal::atomicSwapBool(&locked, false, true);
16-
if (areWeLocked) {
17-
++count;
20+
while(iterations) {
21+
if (tm_internal::atomicSwapBool(&locked, false, true)) {
1822
tm_internal::atomicWritePtr(&initiatingTask, taskManager.getRunningTask());
23+
++count;
1924
return true;
2025
}
2126
else {
22-
taskManager.yieldForMicros(50);
27+
taskManager.yieldForMicros(100);
2328
}
24-
micros -= 50;
29+
--iterations;
2530
}
31+
2632
return false;
2733
}
2834

29-
void ReentrantYieldingLock::unlock() {
30-
if(count != 0) {
31-
--count;
35+
void SimpleSpinLock::unlock() {
36+
if(count == 0) {
3237
return;
3338
}
34-
else {
39+
40+
--count;
41+
42+
if(count == 0) {
3543
tm_internal::atomicWritePtr(&initiatingTask, nullptr);
3644
tm_internal::atomicWriteBool(&locked, false);
3745
}
3846
}
47+

src/ReentrantYieldingLock.h renamed to src/SimpleSpinLock.h

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@
99
#include "TaskManagerIO.h"
1010

1111
/**
12-
* A lock that is intended only for use within tasks, if the lock is taken it will allow task manager to run using
13-
* it's yield for micros call until the lock becomes free. It is a spin lock, so it is safe to use with task manager.
14-
* Unless you want to use the spin behaviour in a specific way, prefer using with TaskSafeLock
12+
* A very simple lock that can be used to provide a very simple mutex like behaviour based on task manager
13+
* atomic constructs. It has the ability to try and spin lock, and also to fully lock in conjunction with
14+
* TaskMgrLock class.
1515
*/
16-
class ReentrantYieldingLock {
16+
class SimpleSpinLock {
1717
private:
1818
tm_internal::TimerTaskAtomicPtr initiatingTask;
19-
tm_internal::TmAtomicBool locked;
2019
volatile uint8_t count;
20+
tm_internal::TmAtomicBool locked;
2121

2222
public:
2323
/**
24-
* Construct a reentrant yielding lock that is designed for use within task manager tasks
24+
* Construct a lock that represents this object
2525
*/
26-
ReentrantYieldingLock() {
26+
SimpleSpinLock() {
2727
initiatingTask = nullptr;
2828
locked = false;
2929
count = 0;
@@ -36,12 +36,18 @@ class ReentrantYieldingLock {
3636
spinLock(0xFFFFFFFFUL);
3737
}
3838

39+
/**
40+
* tries the lock and returns immediately if it was already locked by us.
41+
* @return true if the lock was already owned by us, otherwise false
42+
*/
43+
bool tryLock();
44+
3945
/**
4046
* Attempt to take the lock using a spin wait, it only returns true if the lock was taken.
41-
* @param micros micros to wait
47+
* @param number of iterations to wait, each iteration is at least 100 mics
4248
* @return true if the lock was taken, otherwise false
4349
*/
44-
bool spinLock(unsigned long micros);
50+
bool spinLock(unsigned long iterations);
4551

4652
/**
4753
* Release the lock taken by spinlock or lock. DOES NOT check that the callee is correct so use carefully.
@@ -55,22 +61,24 @@ class ReentrantYieldingLock {
5561

5662
/**
5763
* A wrapper around the task manager locking facilities that allow you to lock within a block of code by putting
58-
* an instance on the stack, for example:
64+
* an instance on the stack. Be very careful not to use yieldForMicros along with this method of locking.
65+
* For example:
5966
*
6067
* ```
61-
* ReentrantYieldingLock myLock;
68+
* SimpleSpinLock myLock;
6269
* void myFunctionToLock() {
6370
* TaskSafeLock(myLock);
6471
* // do some work that needs the lock here. lock will always be released.
72+
* // never use yieldForMicros(..) here or your code may deadlock.
6573
* }
6674
* ```
6775
*/
6876
class TaskMgrLock {
6977
private:
70-
ReentrantYieldingLock& lock;
78+
SimpleSpinLock& lock;
7179

7280
public:
73-
TaskMgrLock(ReentrantYieldingLock& theLock) : lock(theLock) {
81+
TaskMgrLock(SimpleSpinLock& theLock) : lock(theLock) {
7482
lock.lock();
7583
}
7684

src/TaskManagerIO.cpp

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -209,22 +209,22 @@ void TaskManager::runLoop() {
209209
// these until the first one that isn't ready.
210210
TimerTask* tm = tm_internal::atomicReadPtr(&first);
211211

212-
while (tm && tm->isReady()) {
213-
214-
// by here we know that the task is in use. If it's in use nothing will touch it until it's marked as
215-
// available. We can do this part without a lock, knowing that we are the only thing that will touch
216-
// the task. We further know that all non-immutable fields on TimerTask are volatile.
217-
tm_internal::atomicWritePtr(&runningTask, tm);
218-
tm->execute();
219-
tm_internal::atomicWritePtr(&runningTask, nullptr);
220-
removeFromQueue(tm);
221-
if (tm->isRepeating()) {
222-
putItemIntoQueue(tm);
223-
} else {
224-
tm->clear();
225-
tm_internal::tmNotification(tm_internal::TM_INFO_TASK_FREE, TASKMGR_INVALIDID);
212+
while (tm && tm->microsFromNow() == 0) {
213+
if(!tm->isRunning()) {
214+
// by here we know that the task is in use. If it's in use nothing will touch it until it's marked as
215+
// available. We can do this part without a lock, knowing that we are the only thing that will touch
216+
// the task. We further know that all non-immutable fields on TimerTask are volatile.
217+
tm_internal::atomicWritePtr(&runningTask, tm);
218+
tm->execute();
219+
tm_internal::atomicWritePtr(&runningTask, nullptr);
220+
removeFromQueue(tm);
221+
if (tm->isRepeating()) {
222+
putItemIntoQueue(tm);
223+
} else {
224+
tm->clear();
225+
tm_internal::tmNotification(tm_internal::TM_INFO_TASK_FREE, TASKMGR_INVALIDID);
226+
}
226227
}
227-
if(microsToNextTask() > 0) break;
228228
tm = tm->getNext();
229229

230230
#if defined(ESP8266) || defined(ESP32)

tests/taskMgrTests/reentrantLockingTests.cpp

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11

22
#include <AUnit.h>
3-
#include <ReentrantYieldingLock.h>
3+
#include <SimpleSpinLock.h>
44
#include "TaskManagerIO.h"
55
#include "test_utils.h"
66

77
using namespace aunit;
88

9-
ReentrantYieldingLock testLock;
9+
SimpleSpinLock testLock;
1010
taskid_t runTaskId1;
1111
taskid_t runTaskId2;
1212

1313
bool task1RunningPtrCheck = false;
1414
bool task2RunningPtrCheck = false;
15+
bool task3RunningPtrCheck = false;
16+
bool allGood = false;
17+
18+
int runCount1, runCount2, runCount3;
1519

1620
test(testGettingRunningTaskAlwaysCorrect) {
21+
task1RunningPtrCheck = false;
22+
task2RunningPtrCheck = false;
23+
runCount1 = runCount2 = runCount3 = 0;
24+
1725
taskManager.reset();
1826
assertEqual(nullptr, taskManager.getRunningTask());
1927

@@ -26,10 +34,12 @@ test(testGettingRunningTaskAlwaysCorrect) {
2634

2735
task1RunningPtrCheck = taskManager.getRunningTask() == taskManager.getTask(runTaskId1);
2836
}
37+
runCount1++;
2938
});
3039

3140
runTaskId2 = taskManager.scheduleFixedRate(50, [] {
3241
task2RunningPtrCheck = taskManager.getRunningTask() == taskManager.getTask(runTaskId2);
42+
runCount2++;
3343
}, TIME_MICROS);
3444

3545
serdebugF("Scheduled running task check");
@@ -40,8 +50,49 @@ test(testGettingRunningTaskAlwaysCorrect) {
4050

4151
assertTrue(task1RunningPtrCheck);
4252
assertTrue(task2RunningPtrCheck);
53+
assertMore(runCount1, 30);
54+
assertMore(runCount2, 250);
4355
}
4456

4557
test(testReentrantLock) {
58+
taskManager.reset();
59+
task1RunningPtrCheck = false;
60+
task2RunningPtrCheck = false;
61+
task3RunningPtrCheck = false;
62+
allGood = true;
63+
runCount1 = runCount2 = runCount3 = 0;
64+
65+
runTaskId1 = taskManager.scheduleFixedRate(1, [] {
66+
TaskMgrLock locker(testLock);
67+
68+
task1RunningPtrCheck = true;
69+
task2RunningPtrCheck = false;
70+
task3RunningPtrCheck = false;
71+
taskManager.yieldForMicros(1000);
72+
if(task2RunningPtrCheck || task3RunningPtrCheck) {
73+
allGood = false;
74+
}
75+
runCount1++;
76+
});
77+
78+
runTaskId2 = taskManager.scheduleFixedRate(100, [] {
79+
TaskMgrLock locker(testLock);
80+
task2RunningPtrCheck = true;
81+
runCount2++;
82+
}, TIME_MICROS);
83+
84+
taskManager.scheduleFixedRate(50, [] {
85+
TaskMgrLock locker(testLock);
86+
task3RunningPtrCheck = true;
87+
runCount3++;
88+
}, TIME_MICROS);
89+
90+
taskManager.yieldForMicros(millisToMicros(1000));
4691

92+
assertTrue(allGood);
93+
assertTrue(runTaskId1 != TASKMGR_INVALIDID);
94+
assertTrue(runTaskId2 != TASKMGR_INVALIDID);
95+
assertMore(runCount1, 10000);
96+
assertMore(runCount2, 10000);
97+
assertMore(runCount3, 10000);
4798
}

0 commit comments

Comments
 (0)