Ключевое слово C++ volatile с глобальной общей переменной, к которой обращается функция

У меня есть многопоточное приложение C++.

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

Что если, однако, вместо проверки состояния переменной я вызову метод, который возвращает значение переменной? Например:

static int num = 0;

...

void foo()
{
   while(getNum() == 0)
   {
      // do something (or nothing)
   }
}

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

У кого-нибудь есть идеи?

Заранее спасибо,

~ Джулиан

изменить: внутри цикла while я удалил вызов сна и заменил его чем-то общим, например, комментарием, чтобы что-то сделать (или ничего)


person jbu    schedule 06.09.2010    source источник
comment
Проверьте этот вопрос (stackoverflow.com/questions/3148319/).   -  person gablin    schedule 06.09.2010


Ответы (4)


Нет, volatile никогда не понадобится, пока вы выполняете необходимую синхронизацию.

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

В этом конкретном случае sleep, вероятно, будет иметь такой эффект, но в любом случае это не очень хорошая реализация. В num должна быть переменная условия, защитить ее с помощью функции установки, и функция установки должна отправить сигнал на foo.

Что касается конкретного вопроса, то, скрывает ли функция доступ от оптимизации, чрезвычайно зависит от реализации и ситуации. Лучше всего скомпилировать функцию-получатель в отдельном вызове компилятора, но даже в этом случае нельзя гарантировать отсутствие межпроцедурной оптимизации. Например, некоторые платформы могут помещать IR-код в файлы .o и выполнять генерацию кода на этапе "компоновщика".

Отказ от ответственности.

Ключевые слова выше: 1. пока вы выполняете необходимую синхронизацию и 2. вероятно, будет такой эффект.

1: sleep или пустой цикл занятости не являются "необходимой синхронизацией". Это неправильный способ написания многопоточной программы, и точка. Таким образом, volatile может понадобиться в таких случаях.

2: Да, sleep может не учитываться реализацией функции ввода-вывода и даже может быть помечен как чистый и свободный от побочных эффектов. В этом случае потребуется volatile на глобальном уровне. Тем не менее, я сомневаюсь, что действительно были распространены какие-либо реализации, которые разорвали бы такие sleep циклы, поскольку они, к сожалению, распространены.

person Potatoswatter    schedule 06.09.2010
comment
+1: volatile не заменяет правильные конструкции многопоточной синхронизации. - person Oliver Charlesworth; 06.09.2010
comment
Вызов функций синхронизации библиотеки потоков должен позаботиться о том, чтобы сделать недействительными локально «кэшированные» значения и заставить компилятор перезагрузить глобальные значения. Это либо просто неправильно, либо правильно, что требует дальнейшего объяснения... Насколько я понимаю, это неправильно, вызов sleep НЕ заставит компилятор перезагружать значение глобального. На самом деле, будучи частью стандартных библиотек, он может знать, что не имеет побочных эффектов, и поэтому компилятор может сделать вывод, что переменную не нужно перечитывать. - person David Rodríguez - dribeas; 06.09.2010
comment
Прочитайте вопрос/ответы, связанные с Габлином в комментарии к вопросу. - person David Rodríguez - dribeas; 06.09.2010
comment
@David: С другой стороны, этот распространенный вариант использования был бы очень сильным аргументом против намека __pure на серверную часть. - person Potatoswatter; 06.09.2010
comment
@David: сон не имеет ничего общего с моим вопросом ... Мне нужно было заменить sleep () на просто // сделать что-нибудь - person jbu; 06.09.2010
comment
неправильно неправильно неправильно. Во-первых, volatile вообще НЕ влияет на аппаратное кэширование. Это намек абстрактной машине c/c++ на то, что компилятор моделирует свой код с учетом того, что он не может агрессивно кэшировать рассматриваемое значение. В спецификации C++ вообще не содержится требования о том, чтобы неизменяемые тегированные значения вообще когда-либо перезагружались — через точки последовательности или вызовы функций. - person Chris Becke; 06.09.2010
comment
@David: см. open-std.org/ jtc1/sc22/wg21/docs/papers/2004/n1680.pdf , который является лишь первым в длинной череде статей о многопоточности на open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2869.html . К сожалению, в нем нет ссылки на более раннюю, более подробную статью HP, которую я, кажется, потерял. - person Potatoswatter; 06.09.2010
comment
@Potatoswatter: Ваш комментарий, Вызов функций синхронизации библиотеки потоков, какими бы они ни были на вашей платформе, должен позаботиться о том, чтобы сделать недействительными локально кэшированные значения и заставить компилятор перезагрузить глобальные значения. меня интересует. Где я могу узнать больше о том, как это делается? Я знаю, что в JAVA, когда вы блокируете/разблокируете, вы гарантированно увидите самые последние значения переменных, потому что я читал об этом. Откуда я знаю, что это относится к C++? Тем более, что я использую потоки и блокировки из QT Toolkit. Я ничего не видел в API по этому поводу. - person jbu; 06.09.2010
comment
@Chris: я не имею в виду аппаратные кэши… они должны быть согласованными сами по себе, а работа прозрачна. Нет, такого требования нет... но нет и гарантии, что глобальные объекты не будут изменены библиотечными функциями. volatile необходим здесь только в том случае, если библиотека считает, что sleep является чистой функцией без сохранения состояния. Так оно и есть, но это было бы глупым шагом со стороны разработчика. - person Potatoswatter; 06.09.2010
comment
@jbu: многопоточность Java определена лучше, чем C++. Я должен найти лучшую ссылку (эта статья HP конкретно сравнивает Java и C++), но см. приведенный выше ответ Дэвиду. - person Potatoswatter; 06.09.2010
comment
@Potatoswatter: без фактических примитивов синхронизации нет никаких гарантий относительно того, что компилятор может сгенерировать из пользовательского кода выше. Я подробно изучу документы, на которые вы ссылаетесь, но обратите внимание, что n1680 принимается только как документ с обоснованием для объяснения проблемы, а не как руководство для поставщиков компиляторов. - person David Rodríguez - dribeas; 06.09.2010
comment
@David: Верно — см. мою правку. N1680 (и статьи того жанра, который действительно не самый лучший, и я просто искал ссылку) относятся к правильным примитивам синхронизации, а не к sleep. - person Potatoswatter; 06.09.2010
comment
@Potato - это может быть глупым шагом, но вопрос (на мой взгляд) в том, требуется ли ключевое слово volatile для получения определенного поведения путем строгого чтения спецификации С++. Практически говоря, код будет вести себя правильно без «изменчивости». Я не на 100% уверен, что компилятор должен рассматривать библиотечные вызовы как черные ящики, которые могут обращаться к глобальным переменным, которые не были переданы (по какой-то ссылке). - person Chris Becke; 06.09.2010
comment
@Chris: см. Заявление об отказе от ответственности. Что я могу еще сказать. Я действительно не хочу защищать ни volatile, ни sleep, но да, последнее строго требует первого. - person Potatoswatter; 06.09.2010
comment
@David: ах, ключ в том, чтобы читать статьи Бема. open-std.org/jtc1/sc22/ wg21/docs/papers/2007/n2176.html выглядит очень сочно, но сейчас мне нужно идти спать :v( . - person Potatoswatter; 06.09.2010
comment
Мы согласны с тем, что необходима надлежащая синхронизация, но мы по-прежнему не согласны с вашим вторым абзацем: sleep никак не влияет на корректность потока: это совершенно не связано с рассматриваемой проблемой. Прочитав ответ, вы можете понять, что «сон, вероятно, вызовет такой эффект», причем «такой эффект» будет «аннулировать локально кэшированные значения и заставить компилятор перезагружать глобальные значения». что действительно вводит в заблуждение. - person David Rodríguez - dribeas; 06.09.2010
comment
@Chris: Акт создания потока немедленно нарушает §1.9/8, поэтому строго соответствующая реализация C++ вообще не может допускать многопоточность. (Конечно, это изменилось в C++0x.) На мой взгляд, попытка построить программу из циклов ожидания — это не предмет для формализма, а, скорее, очень практичный контроль над ущербом. - person Potatoswatter; 06.09.2010
comment
@David: Действительно, sleep недостаточно для правильности, и не гарантировано произвести такой эффект, но я бы оценил его как вероятный, чтобы он имел правильный эффект, поскольку я Уже сказал и уточнил в дисклеймере. Это такой ужасный способ написания программного обеспечения, что любой, кто делает цикл sleep и беспокоится о volatile, я бы сказал, что его приоритеты наоборот. - person Potatoswatter; 06.09.2010
comment
@ChrisBecke требуется ли ключевое слово volatile для получения определенного поведения путем строгого прочтения спецификации C++. Этот вопрос бессмысленен. - person curiousguy; 27.10.2011
comment
@DavidRodríguez-dribeas «сон может вызвать такой эффект», при этом «такой эффект» заключается в «аннулировании локально кэшированных значений и перезагрузке компилятором глобальных переменных». что действительно вводит в заблуждение. можете ли вы показать один из существующих компиляторов, где это не так? - person curiousguy; 27.10.2011
comment
@curiousguy: Если вы предпочитаете думать о платформе RISC с окнами регистров, компилятор может загрузить значение в один из регистров этой функции, вызвать sleep() и узнать, что значение не было вытеснено из регистра, что означает, что компилятору не нужно перезагружать значение из памяти после вызова, и он может просто использовать его из регистра. - person David Rodríguez - dribeas; 27.10.2011
comment
@DavidRodríguez-dribeas Итак, вы предполагаете, что компилятор понимает семантику sleep(). Есть такая реализация? - person curiousguy; 30.10.2011
comment
@curiousguy: Все дело в том, что вы не можете зависеть от sleep(), вызывающего перезагрузку глобальной памяти. Я не знаю ни одной платформы, в которой компилятор знает, что sleep не изменяет никакого состояния, но это довольно просто: добавьте __attribute(( __pure )) или __attribute(( __const )) в системные заголовки, и компилятор знайте, что он не изменяет глобальное состояние. Обратите внимание, что у компилятора нет встроенных знаний (если вы подумали, что это абсурдно), библиотека может сообщить об этом. - person David Rodríguez - dribeas; 30.10.2011

То, что вы предлагаете, в основном является неправильным использованием «volatile», его реальная цель - сообщить компилятору, что переменная может быть изменена внешним оборудованием или другим процессом в системе, поэтому ее необходимо действительно читать из памяти каждый раз, когда она используется.

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

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

num && x'00000001' 
person James Anderson    schedule 06.09.2010
comment
Да, я думаю, что я должен использовать механизмы синхронизации, чтобы быть уверенным и правильным. - person jbu; 06.09.2010

К сожалению, изменчивая семантика довольно размыта. Концепция volatile на самом деле не предназначалась для использования в многопоточности.

Potatoswatter прав в том, что вызов примитивов синхронизации ОС обычно не позволяет оптимизирующему компилятору поднять чтение num из цикла. Но это работает по той же причине, по которой работает метод доступа... случайно.

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

В corensic мы добавили встроенную функцию в jinx.h, которая делает это более прямым способом. Что-то вроде следующего:

 inline void memory_barrier() { asm volatile("nop" ::: "memory"); }

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

Для вашего примера:

memory_barrier(); в то время как (число == 0) { memory_barrier(); ... }

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

 while (flag == 0) { memory_barrier(); }  // spin
 process data[0..N]

И другой поток делает:

 populate data[0..N]
 memory_barrier();
 flag = 1;

PS. Если вы делаете такие вещи (по сути, создаете свои собственные примитивы синхронизации), выигрыш в производительности может быть большим, но риск качества высок. Jinx особенно хорошо находит ошибки в этих незаблокированных структурах. Поэтому вы можете использовать его или какой-либо другой инструмент для проверки этого материала.

ППС. В сообществе linux есть хороший пост об этом под названием «летучие данные считаются вредными», проверьте его.

person Dave Dunn    schedule 02.10.2010
comment
Но это работает примерно по той же причине, по которой работает метод доступа... случайно. Пожалуйста, объясните. - person curiousguy; 27.10.2011

технически он должен быть помечен как volatile. Компиляторы могут делать все, что хотят, чтобы оптимизировать код, пока он продолжает соответствовать спецификации абстрактной машины С++. Соответствующий компилятор с достаточными ресурсами может встроить все экземпляры getNum, переместить значение num в регистр (или просто, заметив, что оно никогда не изменялось никаким кодом, рассматривать его как константу) на протяжении всего жизненного цикла программы. .

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

person Chris Becke    schedule 06.09.2010
comment
Хорошо, но давайте возьмем немного другую ситуацию. Скажем, статическая переменная num теперь становится закрытой переменной-членом, доступной через getNum, и в результате компилятор не может выполнять встроенный доступ. Нужно ли мне по-прежнему помечать num как volatile?... Я думаю, что правильный ответ в обоих случаях (исходный и новый) заключается в том, что мне нужно ввести синхронизацию через блокировки. - person jbu; 06.09.2010
comment
Я думаю, что большинство компиляторов достаточно умны, чтобы понять, что глобальная переменная в многопоточной программе может быть изменена другим потоком! - person James Anderson; 09.09.2010
comment
Неважно, насколько запутанным вы это сделаете. Абстрактная машина С++ не распознает потоки. Поэтому переменная, которая может быть изменена другим потоком, изменяется вне текущей абстрактной машины. Следовательно, чтобы быть правильным, он ДОЛЖЕН быть помечен как volatile. - person Chris Becke; 09.09.2010
comment
Количество аппаратных регистров здесь не имеет никакого значения. - person curiousguy; 27.10.2011