Задача о читателях и писателях. esp32

Это применительно ко всем 32х битным мк или речь только о есп/рп (не работал с ними, но на сколько знаю - они многоядерные)?

Не всё. Было бы всё просто, если бы обе Ваши задачи в одном ядре исполнялись. А если в разных, то пофиг на их атомарность.

да хоть 100 ядерные. Шина памяти одна. Вот если еще бы и память была много-портовая, тогда можно представить себе накладки. Поэтому для больших ПК или СоК на АРМ всяких - на одноплатниках и прочем, нельзя рассчитывать даже на атомарность записи “int”. А тут все значительно более предсказуемо.

Запутал. Можешь пояснить более понятно?

конечно могу!

ЧТобы на много процессорной системе возникла ситуация “tear” разрыв, должно совпасть много условий. Например ты пишешь 32 бита, но не выровненные. Цикл захвата шины памяти аппаратный. Если ты специально не строишь пример “для ловли глюка”, то все твои флоат или инт - выровнены по границам 4 байт. Так компилятор GCC устроен. И запись такого объекта происходит в один цикл шины памяти. Аппаратно, не от тебя зависит ;). Другое ядро не может прервать процесс.

Можно ли сделать контр пример? конечно! Структуры с опцией pack, байтовый массив со скользящим указателем. Там ты можешь получить int или float не выровненный по 4 байта. Такой будет писаться в два или более цикла шины и тут может попасться “разрыв” со всеми неприятностями. Ты часто пользуешься упакованными структурами или скользящим указателем? Воо-от! Поэтому не забивай себе голову ерундой.
Пора идти бухать!

ЗЫ: есть многопортовая память, на всех больших машинах давно такая, чтобы шина памяти не была узким местом многоядерных систем. Там разрыв возможен вообще всегда. Но в суперкомпах как раз специалист - мой ученый друг - ЕП, тут у него спрашивай лучше.
При многопортовой памяти могут выполняться одновременно несколько запросов к памяти и контроллер сам их оптимизирует. Тут нельзя рассчитывать на отсутствие разрыва даже при записи объекта с разрядностью шины.
Но во всех наших контроллерах, даже многоядерных - можно уверенно! :wink:

2 лайка

жена заняла кухню на переборку будущего холодца из индейки и нет мне места, где пробит клюкву блендером и спиртом её залить!!!
Так что спрашивай, если еще что непонятно.
Система гарантирует “tear-free” чтение и запись объекта с разрядностью шины и выровненного по ней. Компилятор пишет об “андефанед бихейвор” при гонке данных. На практике это означает, что поток-читатель не гарантированно получит актуальное значение. Компилятор сам может решить что-то кешировать или нет и прочее. То есть читатель может получить старое значение переменной, при “дейта рейс” - гонке данных. В наших приложения это чаще всего абсолютно пофигу.

Вот теперь вероятно полная картина. Везде и всегда объект с разрядностью шины и выровненный по этой разрядности - читается и пишется “tear-free”.

Так у меня вопрос был о другом - ТОЛЬКО ли на многопроцессорных системах (или 32 бит вообще) аппаратное атомарное или нет?

Не понял вопрос.
вот же ответ выше:
объект с разрядностью шины и выровненный по этой разрядности - читается и пишется без разрывов

На Меге или Уно шина всего 8 бит. то есть без разрыва пишется 1 байт. На STM32 или RP2040 или ESP32 разрядность шины 32 бита = 4 байта.

Все еше не понятно?

Нет, я понял. Благодарю.

в многопроцессорной системе лучше пользоваться словами “без разрыва” так как “атомарность” - касается одного ядра. Означает то, что операция не прервется на этом конкретном ядре. Но это уже занужное рассуждение о правильности использования терминов. По занудству - это не ко мне. :wink: У нас есть чемпионы в этой области.

Поэтому у меня вопрос и возник. Это ты написал о атомарности))
Либо я не верно понял. Но сейчас вопрос «закрыт». :slight_smile:

Написал тестовый скетч. Четыре задачи пытаются писать в глобальную переменную в одно время. Задачи 1 и 2 на ядре 0, а задачи 3 и 4 на ядре 1.

uint32_t valGlob = 0;

void Task_1(void *parameter) {
  static uint32_t val = 0;
  for (;;) {
    val++;
    valGlob = val;
    Serial.println(valGlob);
    vTaskDelay(pdMS_TO_TICKS(100));  // 100ms
  }
}
void Task_2(void *parameter) {
static uint32_t val = 0;
  for (;;) {
    val += 100;
    valGlob = val;
    Serial.println(valGlob);
    vTaskDelay(pdMS_TO_TICKS(100));  // 100ms
  }
}
void Task_3(void *parameter) {
  static uint32_t val = 0;
  for (;;) {
    val += 10000;
    valGlob = val;
    Serial.println(valGlob);
    vTaskDelay(pdMS_TO_TICKS(100));  // 100ms
  }
}
void Task_4(void *parameter) {
  static uint32_t val = 0;
  for (;;) {
    val += 1000000;
    valGlob = val;
    Serial.println(valGlob);
    vTaskDelay(pdMS_TO_TICKS(100));  // 100ms
  }
}

void setup() {
  Serial.begin(115200);

  xTaskCreatePinnedToCore(Task_1, "Task_1", 2048, NULL, 1, NULL, 0 );
  xTaskCreatePinnedToCore(Task_2, "Task_1", 2048, NULL, 1, NULL, 0 );
  xTaskCreatePinnedToCore(Task_3, "Task_1", 2048, NULL, 1, NULL, 1 );
  xTaskCreatePinnedToCore(Task_4, "Task_1", 2048, NULL, 1, NULL, 1 );
  
}

void loop() { vTaskDelay(pdMS_TO_TICKS(0)); }//предотвращает выделение процессорного времени

Вывод в сериал

102000000
102
1020000
10300
103000000
103
1030000
10400
104000000
104
1040000
105001050000105


105000000
106
1060000
10600
106000000
10710700000010700


1070000
10800
108000000
108
1080000
10900
109000000
109
1090000

12 и 20 строки совсем не то, что должно. Похоже, что вывод трех значений идет в одну строку.
Если задачи на ядре 1, то все нормально.
А как свернуть код?

пример того, что мои объяснения наверное не стоит давать на форуме.
Каждый поток делает ДВА!!! итиегомать, действия. ДВА!!! Он не пишет, а прибавляет. То есть сперва читает, а потом пишет. Между этими действиями может произойти ВСЕ ЧТО УГОДНО.

Я все-таки иногда слишком хорошо думаю о понимании у студента. Так писать нельзя без семафора.

1 лайк

Вижу что твоя жена освободила кухню)

1 лайк

Нет.
Ты описываешь режим по умолчанию. Программист его легко может его изменить. Но точно так же он (программист) может и обезопасить себя, даже изменив режим на packed. Правда, только если сам разрабатывает структуры. Увы, далеко не все разработчики об этом заботятся, в частности, во многих промышленных стандартах структуры используют упакованные как попало данные.

Я тупо запрещаю прерывания на время чтения и записи совместных значений.

Ну что же вы такое говорите.. A memory order? А компилятор поменял порядок инструкций? А процессор его еще раз поменял..эээ, батенька, так не работает. Собственно, я рвлок и улучшал из-за этого.

Пример:

Задача1:

void *buffer;

bool buffer_is_ready;

buffer = 0x123456;

buffer_is_ready = true;

Задача2:

if (buffer_is_ready)

consume(buffer)

Как считаете, может ли задача2 прочитать из переменной buffer - мусор?

Спойлер

Короткий ответ: да, может, если нет никакой синхронизации между задачами.

Развёрнутый:

В FreeRTOS (и вообще на обычных MCU/CPU) простое присваивание buffer = 0x123456; и затем buffer_is_ready = true; не гарантирует, что другой поток (задача) увидит их в том же порядке.

Проблемы могут быть такие:

  1. Оптимизации компилятора.
    Компилятор может держать buffer в регистре или менять порядок store-операций.

  2. Кэширование / write buffer / weak memory ordering.
    На многих архитектурах store в buffer_is_ready может “обогнать” store в buffer.
    Тогда другая задача увидит:

    buffer_is_ready = true
    buffer = <старое значение / мусор>
    
    
  3. Отсутствие volatile или stronger barriers.
    Просто объявить переменные как volatile недостаточно для межпоточной синхронизации.
    volatile ≠ memory barrier.

То есть ситуация:

Task1 writes:

buffer = XXXXX;       // (store1)
buffer_is_ready = 1;  // (store2)

Task2 reads:

if (buffer_is_ready)   // (load2)
   buffer             // (load1)

Нет гарантии, что load1 увидит результат store1.

На ARM Cortex-M — вероятность минимальна, но компилятор reorder может сделать своё дело. На более слабых memory models (ARMv7, ARMv8, RISC-V) — вообще классика.

Если тебе всего лишь надо оперировать с числами, а не с буфферами, то можно таки обойтись БЕЗ синхронизации.

Например, у тебя есть четыре задачи которые пишут и одна - которая читает из нескольких переменных, ну, скажем, int.

Тогда:

Объяви свои переменные - атомиками. Я не уверен как это сделать на Си++, но на Си это будет так:

#include <stdint.h>
#include <stdatomic.h>

_Atomic int dallas1;
_Atomic int dallas2;
_Atomic int dallas3;


Атомарная запись в переменную: atomic_store_explicit( … , memory_order_release);

Атомарное чтение: atomic_load_explicit(…, memory_order_acquire);

Совсем без синхронизации. Это видимо то, на что Влад намекал, говоря об атомарных операциях и ненужности синхронизации.

ЗЫЖ

На многоядерных ЕСПшках ядра шарят память. Т.е. возможна ситуация, когда два ядра ОДНОВРЕМЕННО будут писать в переменную. Что будет в переменной, если она не _Atomic - предсказать будет сложно. На одноядерных ЕСПшках, да, практически любая 32-битная операция (load/store) - атомарная.

1 лайк

Так тоже можно, конечно, но это снижает реалтаймовость да и просрать можно важные прерывания.

ЗЫЖ

в ESP32 в частности и во FreeRTOS в общем есть такая штука, как критическая секция. Примитив такой, синхронизирующий.

это примерно то, что ты делаешь, но только лучше :slight_smile: