Here's an implementation using std::shared_mutex and RAII concepts. The output shows how pending writers are prioritized over reader requests that arrive while the resource is locked for writing by another task. It also shows two reader tasks having access at the same time.
#include <atomic>
#include <shared_mutex>
struct ControlBlock {
friend class WriteLocker;
friend class ReadLocker;
private:
std::shared_mutex accessMutex;
std::atomic_ulong queuedWriters {0};
};
class WriteLocker {
public:
WriteLocker(ControlBlock &controlBlock) : locker(controlBlock.accessMutex, std::defer_lock) {
controlBlock.queuedWriters++;
locker.lock();
controlBlock.queuedWriters--;
}
private:
std::unique_lock<std::shared_mutex> locker;
};
class ReadLocker {
public:
ReadLocker(ControlBlock &controlBlock) : locker(controlBlock.accessMutex, std::defer_lock) {
while (1) {
locker.lock();
if (controlBlock.queuedWriters == 0) {
break;
}
locker.unlock();
taskYIELD();
}
}
private:
std::shared_lock<std::shared_mutex> locker;
};
ControlBlock cb1;
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println();
xTaskCreatePinnedToCore(readerTask, "Task 0", 1500, reinterpret_cast<void *>(0), 3, NULL, CONFIG_ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(writerTask, "Task 1", 1500, reinterpret_cast<void *>(1), 3, NULL, CONFIG_ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(readerTask, "Task 2", 1500, reinterpret_cast<void *>(2), 3, NULL, CONFIG_ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(readerTask, "Task 3", 1500, reinterpret_cast<void *>(3), 3, NULL, CONFIG_ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(writerTask, "Task 4", 1500, reinterpret_cast<void *>(4), 3, NULL, CONFIG_ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(writerTask, "Task 5", 1500, reinterpret_cast<void *>(5), 3, NULL, CONFIG_ARDUINO_RUNNING_CORE);
}
void loop() {
}
void readerTask(void *params) {
auto taskNumber {reinterpret_cast<uint32_t>(params)};
{
Serial.printf("%10lu: Task %lu waiting for Read Access\n", millis(), taskNumber);
ReadLocker lock(cb1);
Serial.printf("%10lu: Task %lu granted Read Access\n", millis(), taskNumber);
vTaskDelay(5000);
Serial.printf("%10lu: Task %lu relinquishing Read Access\n", millis(), taskNumber);
}
vTaskDelete(NULL);
}
void writerTask(void *params) {
auto taskNumber {reinterpret_cast<uint32_t>(params)};
{
Serial.printf("%10lu: Task %lu waiting for Write Access\n", millis(), taskNumber);
WriteLocker lock(cb1);
Serial.printf("%10lu: Task %lu granted Write Access\n", millis(), taskNumber);
vTaskDelay(5000);
Serial.printf("%10lu: Task %lu relinquishing Write Access\n", millis(), taskNumber);
}
vTaskDelete(NULL);
}
Serial Port Output:
3036: Task 0 waiting for Read Access
3036: Task 0 granted Read Access
3036: Task 1 waiting for Write Access
3036: Task 2 waiting for Read Access
3036: Task 3 waiting for Read Access
3036: Task 4 waiting for Write Access
3036: Task 5 waiting for Write Access
8036: Task 0 relinquishing Read Access
8036: Task 1 granted Write Access
13036: Task 1 relinquishing Write Access
13036: Task 4 granted Write Access
18036: Task 4 relinquishing Write Access
18036: Task 5 granted Write Access
23036: Task 5 relinquishing Write Access
23036: Task 2 granted Read Access
23036: Task 3 granted Read Access
28036: Task 2 relinquishing Read Access
28036: Task 3 relinquishing Read Access