2017-02-08 14 views
3

Есть ли способ использовать заборы, чтобы рассуждать о поведении неатомных операций в C11? В частности, я хотел бы сделать код безопасным в ситуациях, когда определенные поля должны быть int s для совместимости со старыми интерфейсами, которые могут, скажем, читать и писать структуры данных в файлы или передавать их в качестве аргументов системных вызовов. Поскольку нет требования, чтобы atomic_int даже был того же размера, что и int, я не могу использовать atomic_int.Заборы с неатомкой в ​​C11

Вот минимальный рабочий пример, к сожалению, приводит к непредсказуемому поведению в соответствии с разделом 5.1.2.4 пункта 25, из-за гонки данных на 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. (Я мог бы сделать readyvolatile, но не вижу никакого предложения в спецификации, что это поможет.)

Смежный вопрос является ли это безопасно записать код выше в любом случае, потому что любая машина мой код будет работать на имеет согласованность кеша? Я знаю, что manythings go wrong, когда программы C11 содержат так называемые доброкачественные расы, поэтому я действительно ищу специфику того, что правдоподобный компилятор и архитектура могут сделать с приведенным выше кодом, а не общие предупреждения о гонках данных и неопределенных поведение.

+0

Все операции синхронизации на C11 работают только с атоматикой (или 'mtx_t'). Таким образом, без какого-либо атома это никогда не сработает. После того, как у вас есть атом, которым вы управляете состоянием, вы можете спорить с отношением «до того времени», эффекты которого становятся видимыми в потоке, даже для эффектов на объекты, не являющиеся атомами. ** Но ** сделать ваши вещи «совместимыми» со старыми интерфейсами безнадежно с атомарностью C11. Они не созданы для этого. Если эти старые интерфейсы действуют неатомно, то состояние вашей системы ударяет дыры в любом доказательстве согласованности, которое вы могли бы придумать. –

+0

@JensGustedt Чтобы быть ясным, мой вопрос касался заборов, а не атомизма. Заборы действительно ограничивают порядок операций неатомной памяти. В этом конкретном примере релиз-забор происходит до получения забора, поэтому нет гонки на 'value', только на' ready'. Так, может быть, ответ заключается в использовании 'memory_order_acq_rel' в обоих случаях? – user3188445

+0

Нет, я думаю, вы ошибаетесь в этом. Две ограждения в разных потоках синхронизируются только через атом. Нет другого способа утверждать, что один забор происходит перед другим, не модифицировав атомный объект, модификация которого воспринимается другим. –

ответ

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 был пойман в бесконечном цикле.
Также неплохо сравнить разницу между инструкциями, испускаемыми компилятором, для атомного и неатомного случая.

Связанный с этим вопрос заключается в том, можно ли все-таки написать вышеприведенный код, поскольку любая машина, на которой будет работать мой код, имеет согласованность кеша?

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