Пример кода IBM, не входящие функции не работают в моей системе - PullRequest
11 голосов
/ 16 января 2020

Я изучал повторный вход в программирование. На этом сайте IBM (действительно хороший). Я основал код, скопированный ниже. Это первый код, который катится по сайту.

Код пытается показать проблемы, связанные с общим доступом к переменным при нелинейной разработке текстовой программы (асинхронность), печатая два значения, которые постоянно меняются в виде " опасный контекст ".

#include <signal.h>
#include <stdio.h>

struct two_int { int a, b; } data;

void signal_handler(int signum){
   printf ("%d, %d\n", data.a, data.b);
   alarm (1);
}

int main (void){
   static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };

   signal (SIGALRM, signal_handler); 
   data = zeros;
   alarm (1);
   while (1){
       data = zeros;
       data = ones;
   }
}

Проблемы возникли, когда я попытался запустить код (или, лучше сказать, не появился). Я использовал g cc версии 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) в конфигурации по умолчанию. Неправильный вывод не происходит. Частота получения «неправильных» парных значений равна 0!

Что в конце концов происходит? Почему нет проблем при повторном входе с использованием глобальных переменных stati c?

Ответы [ 2 ]

17 голосов
/ 16 января 2020

Глядя на проводник компилятора godbolt (после добавления отсутствующего #include <unistd.h>), видно, что почти для любого компилятора x86_64 сгенерированный код использует перемещения QWORD для загрузки ones и zeros в одной инструкции.

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

На сайте IBM написано On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time., что могло бы быть правдой для типичного процессора в 2005 году, но, как показывает код, сейчас не соответствует действительности. Изменение структуры, чтобы иметь два long, а не два целых, показало бы проблему.

Ранее я писал, что это был "Atom c", который был ленивым. Программа работает только на одном процессоре. Каждая инструкция завершится с точки зрения этого процессора (при условии, что ничто иное не изменяет память, такую ​​как dma).

Так что на уровне C не определено, что компилятор выберет один инструкция по написанию структуры, и поэтому может произойти искажение, упомянутое в статье IBM. Современные компиляторы, ориентированные на текущий процессор, используют одну инструкцию. Одной инструкции достаточно, чтобы избежать повреждения однопоточной программы.

12 голосов
/ 17 января 2020

Это не совсем повторный вход ; Вы не запускаете функцию дважды в одном и том же потоке (или в разных потоках). Вы можете получить это с помощью рекурсии или передачи адреса текущей функции в виде функции-указателя функции обратного вызова в другую функцию. (И это не было бы небезопасно, потому что это было бы синхронно).

Это просто обычная ванильная гонка данных UB (Undefined Behavior) между обработчиком сигнала и основным потоком: только sig_atomic_t гарантированно безопасен для этого . Другие могут работать, как в вашем случае, когда 8-байтовый объект может быть загружен или сохранен с одной инструкцией на x86-64, и компилятор выбирает этот asm. (Как показывает ответ @ icarus).

См. Программирование MCU - Сбой оптимизации C ++ O2, в то время как l oop - обработчик прерываний на одноядерном микроконтроллере - это в основном то же самое, что и обработчик сигнала в однопоточной программе. В этом случае результатом UB является то, что нагрузка была поднята из всех oop.

Ваш тестовый случай разрыва фактически происходит из-за гонки данных UB, вероятно, был разработан / протестирован в 32-битном режим или с более старым тупым компилятором, который загружал члены структуры отдельно.

В вашем случае, компилятор может оптимизировать хранилища из бесконечного l oop, потому что никакая UB-бесплатная программа никогда не сможет наблюдать за ними data не является _Atomic или volatile, и в l oop других побочных эффектов нет. Так что никакой читатель не сможет синхронизироваться с этим писателем. Фактически это происходит, если вы компилируете с включенной оптимизацией ( Godbolt показывает пустой l oop в нижней части main). Я также изменил структуру на два long long, и g cc использует одно movdqa 16-байтовое хранилище перед l oop. (Это не гарантировано atomi c, но на практике это происходит практически на всех процессорах, при условии, что он выровнен, или на Intel просто не пересекает границу строки кэша. Почему целое число присваивание для естественно выровненной переменной atomi c на x86? )

Таким образом, компиляция с включенной оптимизацией также нарушит ваш тест и будет показывать вам одно и то же значение каждый раз. C не является переносимым языком ассемблера.

volatile struct two_int также заставит компилятор не оптимизировать их, но не заставит его загружать / сохранять всю структуру атомарно. (Хотя это не остановит его выполнения). Обратите внимание, что volatile делает не обхода UB, но на практике этого достаточно для связи между потоками. и было то, как люди создавали откатанные вручную атомы (вместе с встроенным asm) до C11 / C ++ 11, для нормальной архитектуры ЦП. Они согласованы с кэшем, поэтому volatile на практике в основном аналогичны _Atomic с memory_order_relaxed для чистой загрузки и чистого хранения, если используются для типов, достаточно узких, чтобы компилятор использовал отдельная инструкция, чтобы не порвать. И, конечно, volatile не имеет никаких гарантий от стандарта ISO C против написания кода, который компилируется в тот же формат, используя _Atomic и mo_relaxed.


Если вы была функция, которая выполняла global_var++; на int или long long, который вы запускаете из основных и асинхронно из обработчика сигнала, что было бы способом использовать повторный вход для создания гонки данных UB.

В зависимости от того, как он скомпилирован (в место назначения памяти в c или добавить, или разделить загрузку / вкл / сохранить), это будет атом c или нет относительно сигнала обработчики в одной теме. См. Может ли num ++ быть атомом c для 'int num'? для получения дополнительной информации об атомарности в x86 и в C ++. (Атрибут stdatomic.h и _Atomic в C11 обеспечивает эквивалентную функциональность для шаблона std::atomic<T> в C ++ 11)

В середине инструкции не может возникнуть прерывание или другое исключение, поэтому место назначения памяти add is atomi c wrt. контекст переключается на одноядерный процессор. Только (совместимый с кэшем) DMA-писатель мог «увеличивать» шаг с add [mem], 1 без префикса lock на одноядерном процессоре. Нет никаких других ядер, на которых мог бы работать другой поток.

Так что это похоже на случай сигналов: обработчик сигналов запускает вместо нормальное выполнение потока, обрабатывающего сигнал, поэтому он не может быть обработан в середине одной инструкции.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...