Спсб за разъяснения
Я так понял, это имеет место быть только когда есть более чем одно ядро?
Спсб за разъяснения
Я так понял, это имеет место быть только когда есть более чем одно ядро?
А как же прерывания? Думаю это имеется ввиду в отношении однопоточного цпу.
Вроде как и на одноядерном будет полегче (но гарантий нет) - задачи переключаются поочереди и прямо так, чтобы параллельно - исполняться не будут.
Возможный сценарий: например, одна задача делает А=1, а потом происходит переключение на другую, то там А==1 не гарантировано. Т.е. оно конечно станет единичкой, но не мгновенно. Поэтому какое-то количество времени точка зрения задачи 1 и точка зрения задачи 2 на переменную A будут различаться.А может и не случиться. На x86 не случится. На PowerPC - может. Лучше не рисковать ![]()
На одноядерном многие проблемы решаются просто объявлением переменной volatile. На многоядерном этого недостаточно.
Я сам так делаю и всем рекомендую: когда пишите какую-нибудь функцию в многозадачном коде, всегда задавайте себе вопрос: “а что произойдет, если моя задача будет прервана в строке 123 и процессор переключится на другую задачу?“. А когда многие задачи на многоядерном: “а что произойдет, если такой-то код будет выполнятся одновременно на двух ядрах с небольшой задержкой относительно друг-друга?“ Если возникли сомнения - используйте мутекс.
Если у вас, допустим, есть счетчик (число. 8,16 или 32 бита) , который инкрементируется в трех разных задачах, а читается другими задачами, то нужно обложить чтение\запись этого счетчика мутексами. Вроде как полагается - доступ к shared ресурсу (счетчику) должен быть синхронизирован. Но вот в случае со счетчиками всякими можно опять же обойтись и без мутексов: использовать атомики (atomic_load, atomic_store, atomic_fetch_sub, atomic_fetch_add)
Я всегда думал, что на одноядерном проце, в один момент времени может “видеть” только что-то одно
я не смог навскидку придумать ситуацию, в которой можно продемонстрировать. Так что будем считать, что на одноядерном - попроще :). На ESP32 (двухъядерном) шедулер по умолчанию шедулит задачи на все доступные ядра. Т.е. задача может исполняться то на одном, то на другом ядре. Можно, конечно, при создании задачи жестко ее пришпилить к ядру конкретному и токльо там она и будет исполняться.
Да, @BOOM прав, может не успеть стать 1, как прерывание случится…
Да легко. 1 сначала пишется в регистр, а потом уже в память.
Если прерывание после записи в регистр, то так и будет
P.S. Хотя совместить прерывания и многозадачность это ещё уметь надо…
Вот, нашел в ESP-IDF для ESP32-S3 такие строки:
#define XCHAL_NUM_LOADSTORE_UNITS 1 /* load/store units */ <--- один LS блок
#define XCHAL_NUM_WRITEBUFFER_ENTRIES 4 /* size of write buffer */ <--- store buffer на 4 записи
#define XCHAL_INST_FETCH_WIDTH 4 /* instr-fetch width in bytes */
#define XCHAL_DATA_WIDTH 16 /* data width in bytes */
#define XCHAL_DATA_PIPE_DELAY 1 /* d-side pipeline delay(1 = 5-stage, 2 = 7-stage) */
#include <Arduino.h>
#include <atomic>
// Простейший тест на разделяемую память
std::atomic<int> shared{0};
std::atomic<bool> running{true};
void core1_task(void* p) {
while(running) {
shared.store(1, std::memory_order_relaxed);
delayMicroseconds(10);
}
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println("Минимальный тест visibility");
xTaskCreatePinnedToCore(core1_task, "Core1", 1024, NULL, 1, NULL, 1);
int stale_reads = 0;
for(int i = 0; i < 1000; i++) {
int val = shared.load(std::memory_order_relaxed);
if(val == 0) { stale_reads++;
Serial.print("Core0 прочитал УСТАРЕВШЕЕ значение! #");
Serial.println(stale_reads); } delay(1); }
running = false; delay(100);
Serial.print("Итог: "); Serial.print(stale_reads);
Serial.println(" устаревших чтений из-за отсутствия барьеров");
}
void loop() {}
ЫЫ говорит этот пример все покажет, но можно ли ему верить ?)))
Один интересный пример приводится самой Интел, в их описании архитектуры x86-64:
Рассматривают следующий код (две переменные, X и Y изначально равны 0):
Ядро #0 | Ядро #1
-------------+----------------
mov [X], 1 | mov [Y], 1
mov eax, [Y] | mov ebx, [X]
Для тех, кто не знаком с ассемблером Intel, операция [X] означает “содержимое памяти по адресу X“ или, иначе, “значение переменной X“; EAX и EBX - регистры общего назначения.
Компилятор сгенерировал код приведенный выше и сгенерировал его так, как нам и хотелось. Вопрос: что будет в регистрах EAX и EBX после того как обе задачи закончили выполнение? Логично предположить, что хотя бы один из регистров (eax или ebx) будет содержать 1. А может быть и оба.
Но на самом деле, из миллиона итераций, пару сотен раз происходит странное: оба регистра EAX и EBX равны нулю (!). Как это вообще возможно? Возможно, и есть даже код, который демонстрирует эту странность на Intel: Win и POSIX версии.
Происходит так потому, что иногда процессор исполняет код так:
Ядро #0 | Ядро #1
-------------+----------------
mov eax, [Y] | mov ebx, [X]
mov [X], 1 | mov [Y], 1
Переставляет запись и чтение из памяти.
С точки зрения CPU операция чтения и следующая за най операция записи никак не связаны между собой, поэтому процессор “имеет право переставлять LOAD после STORE, если они не зависят друг от друга“.
Что процессор иногда и делает.
В результате получаем ноль в обоих регистрах. В коде-тесте по ссылке выше, на Win32, средние значения были около 80 перестановок на 500000 итераций.
Спасибо, но если я правильно понял, то найти подробную и понятную информацию об устройстве процессора непросто. А так как мне это просто было любопытно из интереса, а не по необходимости, то я оставил это. Для ясности - я ожидал увидеть документ типа даташита на ATmega328 где приведена диаграмма выполнения инструкции по тактам, по которой понятно, что сколько времени занимает и от чего зависит. Тут же чтобы понять (или предположить), как это работает, надо затратить слишком много усилий.
К сожалению, именно для ESP32 эта информация клнфиденциальная. Что-то утекло в сеть, конечно, код кое-какой тоже утек. Espressif не имеет права публиковать ничего из этого, кроме своих расширений. Свои расширения (TIE/PIE Instructions, векторыне и прочие) они опубликовали, со скрипом.
Для интела - навалом. Да и для других процессоров тоже. ESP32 уникален тем, что ежегодно продается 1 млрд этих чипов, а документацию приходится искать с факелами. И ИИ тут не помошник - сплошные галлюцинации на тему ESP32
Так именно так оно и будет работать.
То есть программа после преобразования (компилятором, оптимизатором, процессором) будет выдавать в точности такой же результат (возможно, кроме времени выполнения), как и без преобразования.
Ну а если буквально “ровно так, как написал” - отключай оптимизатор и будет тебе счастье.
Насчет задач - не скажу, а отдельные инструкции именно параллельно и выполняются. Называется конвейер. Вот именно для конвейера бывает целесообразно изменить порядок выполнения инструкций.
Мне кажется, для Интела последним процессором, для которого существовала открытая подробная документация, был Pentium. Хотя, возможно, я и отстал от жизни.
Так он до сих пор и остался (честных 66 МHz), всё остальное маркетинг про их гигагерцы с каруселью рециклинга. Пока матери медные, как бы не были они правильно разведены, выше 33MHz по периферии физика не позволяет. А переходить на оптику производители пока не торопятся. А зачем, пока покупают, вкладывать ни кто не собирается в разработку.
Так иногда и делаю, на время, для диагностики. Т.е. если сбой после отключения оптимизатора пропадает, значит я где-то невнятно написал, и, оптимизатор “исправил”.
Хорошо бы, если так же можно и в процессоре вкл./ выкл. эту опцию, но , пока, не нашёл об этом.
Ну, может, конечно, и остается…
Только во что такое “честные 66 МГц”? Этот тот, который 5-вольтовая “печка”?
И вообще, какое отношение имеет частота внешней шины к частоте “камня”? И при чем здесь “честность”.
Кстати, архитектура Pentium II существенно отличается от Pentium. Причем, изменение порядка выполнения инструкций начинается как раз со “второго”, в “первом” этого не было.
Вот тут есть немножко. Последний апдейт от 2025 года. Про все кишки написано, про конвееры и прочее. Глава номер 2 как раз on-topic
Занимательное чтиво, жалко у меня его не было в 2007 году :))
Есть такая старая книжка, The Art of Multiprocessor Programming, которая будет интересна, может быть, ЕвгениюП и @MMM . Первому - за академичность и широту охвата материала а второму - за Си++.
Вот перевод одной из глав, вернее, начала главы.
Когда процессор (практически любой, современный) записывает значение в память, это значение сначала сохраняется в кэше и
помечается как грязное (dirty), то есть такое, которое в конечном итоге должно быть записано обратно в основную память.
На большинстве современных процессоров запросы на запись не применяются к памяти сразу в момент их выполнения. Вместо этого они помещаются в аппаратную очередь, называемую буфером записи (write buffer или store buffer), и затем применяются к памяти совместно, позднее.
Буфер записи даёт два преимущества. Во-первых, часто эффективнее выполнять сразу несколько запросов вместе (т.н. batching). Во-вторых, если поток записывает данные по одному и тому же адресу несколько раз, более ранние запросы могут быть отброшены, что позволяет избежать лишнего обращения к памяти (т.н. write absorption).
Использование буферов записи имеет очень важное следствие: порядок, в котором операции чтения и записи реально выполняются в памяти, не обязательно совпадает с порядком, в котором они записаны в программе.
Всё это хорошо, конечно, жаль только, что не предусматривается команда, (возможно я не нашёл ) чтобы можно было отменить, по желанию программиста, эти “навороты”, если, и, когда это надо.))