Skip to content

Commit f3aa7dd

Browse files
author
dave
committed
#25 add support for acquiring the currently running task and reentrant locking
1 parent d336ca2 commit f3aa7dd

File tree

5 files changed

+188
-1
lines changed

5 files changed

+188
-1
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
#include <TaskManagerIO.h>
3+
#include <ReentrantYieldingLock.h>
4+
5+
// this is the global lock that can only be
6+
ReentrantYieldingLock myLock;
7+
volatile int myVar = 0;
8+
9+
void log(const char* str, int val) {
10+
Serial.print(millis());
11+
Serial.print(' ');
12+
Serial.println(str);
13+
}
14+
15+
void nestedFunction() {
16+
TaskMgrLock locker(myLock);
17+
log("in nested function", myVar);
18+
}
19+
20+
void setup() {
21+
Serial.begin(115200);
22+
23+
taskManager.scheduleFixedRate(1000, [] {
24+
TaskMgrLock locker(myLock);
25+
log("start task function", myVar);
26+
27+
// the nested function will lock again, which is fine because it's in the same task
28+
nestedFunction();
29+
30+
// now we release taskmanager to run other tasks, the will not be able to take our lock
31+
taskManager.yieldForMicros(millisToMicros(500));
32+
33+
// now we have the context back lock again
34+
nestedFunction();
35+
log("exit task function", myVar);
36+
});
37+
38+
taskManager.scheduleFixedRate(5, [] {
39+
// here we have another task that locks, it will need to acquire the lock exclusively before entering
40+
TaskMgrLock locker(myLock);
41+
// do something that's unlikely to be optimised out, to waste some time
42+
for(int i=0; i < 100; i++) {
43+
myVar++;
44+
}
45+
});
46+
}
47+
48+
void loop() {
49+
taskManager.runLoop();
50+
}

src/ReentrantYieldingLock.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2018-present https://www.thecoderscorner.com (Nutricherry LTD).
3+
* This product is licensed under an Apache license, see the LICENSE file in the top-level directory.
4+
*/
5+
6+
#include "ReentrantYieldingLock.h"
7+
8+
bool ReentrantYieldingLock::spinLock(unsigned long micros) {
9+
// if our task already owns the lock then we are good.
10+
if(locked && initiatingTask && taskManager.getRunningTask() == initiatingTask) return true;
11+
12+
// 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;
18+
tm_internal::atomicWritePtr(&initiatingTask, taskManager.getRunningTask());
19+
return true;
20+
}
21+
else {
22+
taskManager.yieldForMicros(50);
23+
}
24+
micros -= 50;
25+
}
26+
return false;
27+
}
28+
29+
void ReentrantYieldingLock::unlock() {
30+
if(count != 0) {
31+
--count;
32+
return;
33+
}
34+
else {
35+
tm_internal::atomicWritePtr(&initiatingTask, nullptr);
36+
tm_internal::atomicWriteBool(&locked, false);
37+
}
38+
}

src/ReentrantYieldingLock.h

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) 2018-present https://www.thecoderscorner.com (Nutricherry LTD).
3+
* This product is licensed under an Apache license, see the LICENSE file in the top-level directory.
4+
*/
5+
6+
#ifndef TASKMANAGERIO_RENTRANTYIELDINGLOCK_H
7+
#define TASKMANAGERIO_RENTRANTYIELDINGLOCK_H
8+
9+
#include "TaskManagerIO.h"
10+
11+
/**
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
15+
*/
16+
class ReentrantYieldingLock {
17+
private:
18+
tm_internal::TimerTaskAtomicPtr initiatingTask;
19+
tm_internal::TmAtomicBool locked;
20+
volatile uint8_t count;
21+
22+
public:
23+
/**
24+
* Construct a reentrant yielding lock that is designed for use within task manager tasks
25+
*/
26+
ReentrantYieldingLock() {
27+
initiatingTask = nullptr;
28+
locked = false;
29+
count = 0;
30+
}
31+
32+
/**
33+
* Take the lock waiting the longest possible time for it to become available.
34+
*/
35+
void lock() {
36+
spinLock(0xFFFFFFFFUL);
37+
}
38+
39+
/**
40+
* Attempt to take the lock using a spin wait, it only returns true if the lock was taken.
41+
* @param micros micros to wait
42+
* @return true if the lock was taken, otherwise false
43+
*/
44+
bool spinLock(unsigned long micros);
45+
46+
/**
47+
* Release the lock taken by spinlock or lock. DOES NOT check that the callee is correct so use carefully.
48+
*/
49+
void unlock();
50+
51+
uint8_t getLockCount() const { return count; }
52+
53+
bool isLocked() const { return locked; }
54+
};
55+
56+
/**
57+
* 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:
59+
*
60+
* ```
61+
* ReentrantYieldingLock myLock;
62+
* void myFunctionToLock() {
63+
* TaskSafeLock(myLock);
64+
* // do some work that needs the lock here. lock will always be released.
65+
* }
66+
* ```
67+
*/
68+
class TaskMgrLock {
69+
private:
70+
ReentrantYieldingLock& lock;
71+
72+
public:
73+
TaskMgrLock(ReentrantYieldingLock& theLock) : lock(theLock) {
74+
lock.lock();
75+
}
76+
77+
~TaskMgrLock() {
78+
lock.unlock();
79+
}
80+
};
81+
82+
#endif //TASKMANAGERIO_RENTRANTYIELDINGLOCK_H

src/TaskManagerIO.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ TaskManager::TaskManager() : taskBlocks {} {
5353
lastInterruptTrigger = 0;
5454
taskBlocks[0] = new TaskBlock(0);
5555
numberOfBlocks = 1;
56+
runningTask = nullptr;
5657
tm_internal::atomicWriteBool(&memLockerFlag, false);
5758
}
5859

@@ -168,10 +169,12 @@ void TaskManager::cancelTask(taskid_t taskId) {
168169
void TaskManager::yieldForMicros(uint32_t microsToWait) {
169170
yield();
170171

172+
auto* prevTask = runningTask;
171173
unsigned long microsStart = micros();
172174
do {
173175
runLoop();
174176
} while((micros() - microsStart) < microsToWait);
177+
tm_internal::atomicWritePtr(&runningTask, prevTask);
175178
}
176179

177180
void TaskManager::dealWithInterrupt() {
@@ -180,9 +183,11 @@ void TaskManager::dealWithInterrupt() {
180183

181184
auto lastSlot = taskBlocks[numberOfBlocks - 1]->lastSlot() + 1;
182185
for(taskid_t i=0; i<lastSlot; i++) {
183-
auto task = getTask(i);
186+
auto* task = getTask(i);
184187
if(task->isInUse() && task->isEvent()) {
188+
tm_internal::atomicWritePtr(&runningTask, task);
185189
task->processEvent();
190+
tm_internal::atomicWritePtr(&runningTask, nullptr);
186191
removeFromQueue(task);
187192
if(task->isRepeating()) {
188193
putItemIntoQueue(task);
@@ -214,7 +219,9 @@ void TaskManager::runLoop() {
214219
// by here we know that the task is in use. If it's in use nothing will touch it until it's marked as
215220
// available. We can do this part without a lock, knowing that we are the only thing that will touch
216221
// the task. We further know that all non-immutable fields on TimerTask are volatile.
222+
tm_internal::atomicWritePtr(&runningTask, tm);
217223
tm->execute();
224+
tm_internal::atomicWritePtr(&runningTask, nullptr);
218225
removeFromQueue(tm);
219226
if(tm->isRepeating()) {
220227
putItemIntoQueue(tm);

src/TaskManagerIO.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class TaskManager {
9898
volatile InterruptFn interruptCallback;
9999

100100
tm_internal::TmAtomicBool memLockerFlag; // memory and list operations are locked by this flag using the TmSpinLocker
101+
tm_internal::TimerTaskAtomicPtr runningTask;
101102
public:
102103
/**
103104
* On all platforms there is a default instance of TaskManager called taskManager. You can create other instances
@@ -279,6 +280,15 @@ class TaskManager {
279280
if(maybeTask == nullptr) return 600 * 1000000U; // wait for 10 minutes if there's nothing to do
280281
else return maybeTask->microsFromNow();
281282
}
283+
284+
/**
285+
* Gets the currently running task, this is only useful for places where re-entrant checking is needed to ensure
286+
* the same task is taking the lock again for example. Never change the task state in this call, and also never
287+
* store this pointer.
288+
* @return a temporary pointer to the running task that lasts as long as it is running.
289+
*/
290+
tm_internal::TimerTaskAtomicPtr getRunningTask() { return runningTask; }
291+
282292
private:
283293
/**
284294
* Finds and allocates the next free task, once this returns a task will either have been allocated, making task

0 commit comments

Comments
 (0)