Заборы с неатомной физикой в С11
Есть ли способ использовать ограждения, чтобы рассуждать о поведении неатомарных операций в C11? В частности, я хотел бы сделать код безопасным в ситуациях, когда необходимо заполнить определенные поля. int
s для совместимости со старыми интерфейсами, которые могут, скажем, считывать и записывать структуры данных в файлы или передавать их в качестве аргументов системного вызова. Поскольку нет требования, чтобы atomic_int
даже быть такого же размера, как int
Я не могу использовать atomic_int
,
Вот минимальный рабочий пример, который, к сожалению, приводит к неопределенному поведению согласно пункту 25 раздела 5.1.2.4 из-за гонки данных на ready
:
#include <stdatomic.h>
#include <stdio.h>
#include <threads.h>
int ready; /* purposely NOT _Atomic */
int value;
void
p1()
{
value = 1;
atomic_thread_fence(memory_order_release);
ready = 1;
}
void
p2(void *_ignored)
{
while (!ready)
;
atomic_thread_fence(memory_order_acquire);
printf("%d\n", value);
}
int
main()
{
thrd_t t;
thrd_create(&t, p2, NULL);
p1();
thrd_join(&t, NULL);
}
Мой конкретный вопрос: возможно ли исправить вышеуказанный код, чтобы гарантировать печать? 1
без изменения ready
для _Atomic
, (Я мог бы сделать ready
volatile
, но не вижу в спецификации никаких предположений, что это поможет.)
Смежный вопрос: безопасно ли в любом случае писать приведенный выше код, потому что любая машина, на которой будет работать мой код, имеет когерентность кэша? Я знаю, что многие вещи идут не так, когда программы C11 содержат так называемые доброкачественные расы, поэтому я действительно ищу особенности того, что вероятный компилятор и архитектура могут сделать с приведенным выше кодом, а не общие предупреждения о гонках данных и неопределенных поведение.
1 ответ
Есть ли способ использовать ограждения, чтобы рассуждать о поведении неатомарных операций в C11?
То, как вы используете ограждения, является правильным, но если вы хотите иметь возможность рассуждать о поведении программы, вы несете ответственность за обеспечение строгого порядка изменения между потоками между хранилищем (1) и ready
и нагрузка (1) от него. Обычно это где atomic
переменная вступает в игру. Согласно стандарту C11 у вас есть гонка данных на ready
(как вы указали) и неопределенное поведение - это то, что вы можете ожидать.
Мой конкретный вопрос: возможно ли исправить приведенный выше код, чтобы гарантировать печать 1, не переходя в готовый к _Atomic. (Я мог бы подготовить изменчивый, но не вижу никаких предположений в спецификации, что это помогло бы.)
Ответ, соответствующий стандартам, - "нет", и поскольку стандарт не поддерживает ваше дело, вы не найдете ничего, связанного с volatile
в данном контексте.
Тем не менее, стандарт является строгим по назначению, учитывая, что одной из целей является поддержка совместимости со многими архитектурами. Это не означает, что гонка данных всегда будет приводить к проблемам на каждой платформе.
Проблемы с использованием неатомарных типов в общем контексте, однако, сложны. Люди иногда считают, что если процессорные операции с такими типами, как int
неделимы, его можно использовать как замену atomic_int
, Это не так, потому что "атомная" - это концепция с более широкими последствиями:
неделимое чтение / запись - они применяются к обычным типам на многих платформах.
ограниченная оптимизация - преобразования компилятора могут действительно вызывать неопределенное поведение многими неожиданными способами. Компилятор может переупорядочить операции с памятью, объединить переменную с другой в той же ячейке памяти, удалить переменную из цикла, сохранить ее в регистре и т. Д. Вы можете предотвратить многое из этого, объявив свою переменную
volatile
поскольку это накладывает ограничения на то, что компилятор может делать в отношении оптимизации.синхронизация данных между ядрами - в вашем случае это обрабатывается ограждениями при условии строгого межпоточного упорядочения
ready
между магазином и грузом. С реальнымatomic_int
Вы могли бы использовать расслабленные операции.
Работает ли ваш код, зависит от платформы и компилятора, но, по крайней мере, объявите ready
флаг volatile
, Я сделал тестовый запуск на X86_64 с gcc -O3
оптимизация компилятора и без volatile
это было поймано в бесконечную петлю.
Также неплохо сравнить разницу между инструкциями, генерируемыми компилятором, для атомарного и неатомного случая.
Смежный вопрос: безопасно ли в любом случае писать приведенный выше код, потому что любая машина, на которой будет работать мой код, имеет когерентность кэша?
Вам определенно нужна когерентность кэша, потому что системы, которые его не поддерживают, общеизвестно сложны для программирования. То, как вы это написали, почти наверняка не сработает без когерентности кеша.