1 Writer, Many Readers

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