2013-02-22 1 views
4

Скажем, я определить следующий C++ объект:Как «неопределенное» состояние гонки может быть?

class AClass 
{ 
public: 
    AClass() : foo(0) {} 
    uint32_t getFoo() { return foo; } 
    void changeFoo() { foo = 5; } 
private: 
    uint32_t foo; 
} aObject; 

Объект используется двумя нитями, T1 и T2. T1 постоянно вызывает getFoo() в цикле, чтобы получить номер (который всегда будет 0, если changeFoo() не был вызван раньше). В какой-то момент T2 вызывает changeFoo(), чтобы изменить его (без какой-либо синхронизации потоков).

Есть ли практический вероятность того, что значения, полученные когда-либо T1 будет отличаться от 0 или 5 с современных компьютерных архитектур и компиляторы? Весь код ассемблера, который я исследовал до сих пор, это использование 32-разрядных чтений и записей в памяти, что, похоже, сохраняет целостность операции.

Как насчет других примитивных типов?

Практическая означает, что вы можете дать пример существующей архитектуры или стандартный совместимый компилятор, где это (или аналогичная ситуация с другим кодом) теоретически возможно. Я оставляю слово modern немного субъективно.


Edit: Я вижу много людей, заметив, что я не должен ожидать, 5 следует читать когда-либо. Для меня это прекрасно, и я не сказал, что делаю (хотя спасибо за указание на этот аспект). Мой вопрос был больше о том, какое нарушение целостности данных может произойти с вышеуказанным кодом.

+0

Последний абзац не имеет смысла. ** Практический ** пример чего-то, где теоретически возможен определенный результат **? Я предлагаю вам удалить одно из этих двух слов. :) – jalf

+0

Теоретически ** возможно, что это произойдет на ** каждой ** существующей архитектуре с ** каждым ** стандартным компилятором, потому что компилятор, совместимый со стандартами, может делать все, что ему нравится, когда он сталкивается UB. На практике компиляторы склонны прощать о UB, но теоретически * возможно, что они делают всевозможные другие странные вещи. – jalf

+0

@jalf Здесь не имеет никакого значения, так как код не будет работать должным образом (что поток чтения в конце концов увидит 5) для любой архитектуры, которую я знаю (конечно же, не с VC++ под Windows, g ++ под Linux или Sun CC под Solaris). –

ответ

3

На практике все основные 32-разрядные архитектуры выполняют 32-разрядные чтения и записи атомарно. Вы никогда не увидите ничего, кроме 0 или 5.

+1

Это неправильно. Компилятор может изменить вашу программу, чтобы изменения никогда не были видны другим потокам. – inf

+4

@bamboon: Я сделал две претензии. Кто из них ложный? –

+0

Извините, за то, что я неточен, я имел в виду более позднюю часть. Два потока, обращающиеся к переменной, где одна - запись, а другая - результат чтения в состоянии гонки, который является UB на стандарт. – inf

0

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

См., @Matthieu M. Отвечать за менее саркастическую версию, чем эта. Я не удалю это, поскольку, по-моему, комментарии важны для обсуждения.

+0

Это относится к названию вопроса, но не к самому вопросу. –

+0

Это так, потому что остальное не имеет значения в присутствии UB. – inf

+0

Где именно находится UB? –

0

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

+0

Я не помню никаких спецификаций о том, что «условие гонки» подразумевает «неопределенное поведение» таким образом, что термин используется спецификациями. Или это? – Andrew

+4

@ Andrew К сожалению, да. [intro.multithread] §21: Выполнение программы содержит гонку данных, если она содержит два конфликтующих действия в разных потоках, по крайней мере один из которых не является атомарным, и не происходит до другого. Любая такая гонка данных приводит к неопределенному поведению. – Angew

+0

У меня возникли проблемы с поиском этой части в спецификациях pre-C++ 11. Это означает, что ваш ответ правильный только для тех компиляторов, которые соответствуют C++ 11 (и при условии, что я активирую функции C++ 11, такие как gcc). Для тех, кто этого не делает, это не неопределенное поведение. Но, может быть, я взял неверный документ, не так ли? – Andrew

2

На практике (для тех, кто не читал этот вопрос), любая потенциальная проблема сводится к тому, является ли операция сохранения для unsigned int является атомарной операцией, которая, на большинство (если не все) машины вы будете скорее всего, код для написания, это будет.

Обратите внимание, что это не указано стандартом; он специфичен для архитектуры, на которую вы нацеливаетесь. Я не могу представить себе сценарий, в котором вызывающий поток будет красным ничего, кроме 0 или 5.

Что касается названия ... Я не знаю разной степени «неопределенного поведения». UB - UB, это двоичное состояние.

+0

Это неправильно. Компилятор может изменить вашу программу, чтобы изменения никогда не были видны другим потокам. – inf

+0

@bamboon: Помогите объяснить, как? –

+0

Два потока, обращающиеся к переменной, где одна является записью, а другая - результатами чтения в состоянии гонки, который является UB в стандарте. – inf

2

Не уверен, что вы ищете. На большинстве современных архитектур существует очень четкая возможность, что getFoo() всегда возвращает 0, даже после того, как был вызван changeFoo. С почти любым достойным компилятором почти гарантировано, что getFoo() всегда будет возвращать одно и то же значение, независимо от любых вызовов changeFoo, если он вызывается в узком цикле.

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

И, наконец, являются 16 разрядных процессоров, и также может быть возможность с некоторыми компиляторами, что uint32_t не выровнены, так что доступы не будет атомарным. (Конечно, вы меняете только биты в одном из байтов, так что это может не быть проблемой.)

+0

+1 здесь, он отвечает на вопрос (я не видел, чтобы вы отправили ответ во время всех комментариев). –

9

На практике вы не увидите ничего, кроме 0 или 5, насколько я знаю (возможно, некоторые странная 16-битная архитектура с 32 битами int, где это не так).

Однако, если вы действительно видите, что 5 вообще не гарантируется.

Предположим, что я являюсь компилятором.

Я вижу:

while (aObject.getFoo() == 0) { 
    printf("Sleeping"); 
    sleep(1); 
} 

Я знаю, что:

  • printf не может изменить aObject
  • sleep не может изменить aObject
  • getFoo не меняет aObject (благодаря четкости рядный)

И поэтому я смело могу преобразовать код:

while (true) { 
    printf("Sleeping"); 
    sleep(1); 
} 

Поскольку нет никто другой доступ к aObject во время этого цикла, в соответствии со стандартом C++.

Это то, что не определено поведение означает: взорванные ожидания.

+0

И тогда это действительно сводится к тому, «вы готовы рисковать своей правильностью вашей программы, исходя из предположения, что компилятор не является и не будет достаточно умным, чтобы сделать эту оптимизацию». :) – jalf

+1

Но вопрос: «Существует ли какая-либо практическая вероятность того, что значения, когда-либо полученные T1, будут отличаться от 0 или 5 с современными компьютерными архитектурами и компиляторами?» *, И ответ «нет», если загрузка и хранение являются атомными. –

+1

@EdS .: ** и ** до тех пор, пока ни один из них не будет оптимизирован (поскольку в противном случае они вообще не достигнут центрального процессора, и тогда не имеет значения, будут ли они выполняться атомарно или нет) , но, как указывает этот ответ, оптимизатору разрешено его оптимизировать – jalf

2

Есть ли практическая вероятность того, что значения, когда-либо полученные T1, будут отличаться от 0 или 5 с современными компьютерными архитектурами и компиляторами? Как насчет других примитивных типов?

Уверен - нет никакой гарантии, что все данные будут записаны и прочитаны атомным способом. На практике, вы можете получить чтение, которое произошло во время частичной записи. Что может быть прервано, и когда это произойдет, зависит от нескольких переменных. Таким образом, на практике результаты могут легко варьироваться в зависимости от размера и выравнивания типов. Естественно, эта разница также может быть введена, так как ваша программа перемещается с платформы на платформу и изменяется как ABI.Кроме того, наблюдаемые результаты могут изменяться по мере добавления оптимизаций и введения других типов/абстракций. Компилятор может оптимизировать большую часть вашей программы; возможно, полностью, в зависимости от объема экземпляра (еще одна переменная, которая не рассматривается в ОП).

Помимо оптимизаторов, компиляторов и конкретных аппаратных конвейеров: Ядро может даже влиять на способ обработки этой области памяти. Ваша программа Гарантия где хранится память каждого объекта? Возможно нет. Память вашего объекта может существовать на отдельных страницах виртуальной памяти - какие шаги предпринимает ваша программа, чтобы обеспечить постоянное считывание и запись памяти для всех платформ/ядер? (нет, по-видимому)

Вкратце: если вы не можете играть по правилам, определенным абстрактной машиной, вы не должны использовать интерфейс указанной абстрактной машины (например, вы должны просто понимать и использовать сборку, если спецификация абстрактного текста C++ машина по-настоящему неадекватна для ваших нужд - очень маловероятна).

Весь код ассемблера, который я исследовал до сих пор, использовал чтение и запись в 32-битной памяти, что, как представляется, сохраняет целостность операции.

Это очень мелкое определение «целостности». Все, что у вас есть, - это (псевдо) последовательная согласованность. Кроме того, компилятору нужно вести себя так, как если бы в таком сценарии, который далек от строгой последовательности. Неглубокое ожидание означает, что даже если компилятор фактически не делал оптимизации разбиения и выполнял чтение и запись в соответствии с каким-то идеалом или намерением, результат был бы практически бесполезным - ваша программа будет наблюдать изменения, обычно длинные после его появления ,

Тема не имеет значения, учитывая то, что конкретно вы можете Гарантия.

+1

Последнее предложение является очень важным. – inf

+0

@justin: Я не могу согласиться с последним абзацем: представьте случай (и на самом деле это был корень моего вопроса), где у меня есть несколько потоков, которые выполняют задачи в циклах, и они подсчитывают (=> write) количество просто для статистики, и я хочу отобразить (=> прочитать) этот номер, чтобы увидеть статистику. Для меня статистически не имеет значения, будет ли это число 1000000 или 1000042. Если несинхронизированное чтение-запись может привести к сбою программы, это проблема для меня. Но если это просто заставляет меня видеть поддельный номер, я могу жить с ним. Поэтому результат МОЖЕТ быть практически полезным. – Andrew

+0

@Andrew Как это имеет смысл? Почему вы хотите создать статистику, которая может быть абсолютно неправильной. Какова цель создания ложных данных? Они могут потенциально отличаться цифрами, намного большими, чем 42. – inf