Ранее я написал некоторый очень простой многопоточный код, и я всегда знал что в любое время могло быть контекстное переключение прямо в середине того, что я делаю, таким образом, я всегда охранял доступ совместно используемые переменные через класс CCriticalSection, который вводит критический раздел по конструкции и оставляет его на разрушении. Я знаю, что это довольно агрессивно, и я ввожу и оставляю критические разделы вполне часто и иногда в высшей степени (например, в начале функции, когда я мог поместить CCriticalSection в более трудном блоке кода), но мой код не отказывает, и это работает достаточно быстро.
На работе мой многопоточный код должен быть более трудным, только блокирующий/синхронизирующий на самом низком необходимом уровне.
На работе я пытался отладить некоторый многопоточный код, и я столкнулся с этим:
EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);
Теперь, m_bSomeVariable
Win32 BOOL (не энергозависимый), который насколько я знаю, определяется, чтобы быть интервалом, и на чтении x86 и записи, что эти значения являются единственной инструкцией, и так как контекстные переключения происходят на границе инструкции затем нет никакой потребности в синхронизации этой операции с критическим разделом.
Я провел еще некоторое исследование онлайн, чтобы видеть, не была ли для этой операции нужна синхронизация, и я придумал два сценария, которые это сделало:
Я полагаю, что номер 1 может быть решен с помощью "энергозависимого" ключевого слова. В VS2005 и позже компилятор C++ окружает доступ к этому переменному использованию барьеры памяти, гарантируя, что переменная всегда полностью пишется/читается в главную системную память перед использованием его.
Номер 2, который я не могу проверить, я не знаю, почему выравнивание байта имело бы значение. Я не знаю x86 системы команд, но делает mov
потребность, которой дадут 4 байта, выровняла адрес? Если не необходимо использовать комбинацию инструкций? Это представило бы проблему.
Так...
ВОПРОС 1: использование "энергозависимого" ключевого слова (implicity использующий барьеры памяти и подсказывающий компилятору для не оптимизации этот код) освобождают программиста от потребности синхронизироваться 4-byte/8-byte на x86/x64 переменной между операциями чтения-записи?
ВОПРОС 2: существует ли явное требование что переменная быть 4-byte/8-byte выровненное?
Я сделал еще некоторое рытье в наш код и переменные, определенные в классе:
class CExample
{
private:
CRITICAL_SECTION m_Crit1; // Protects variable a
CRITICAL_SECTION m_Crit2; // Protects variable b
CRITICAL_SECTION m_Crit3; // Protects variable c
CRITICAL_SECTION m_Crit4; // Protects variable d
// ...
};
Теперь, мне это кажется чрезмерным. Я думал, что критические разделы синхронизировали потоки между процессом, поэтому если у Вас есть тот, можно ввести его, и никакой другой поток в том процессе не может выполниться. Нет никакой потребности в критическом разделе для каждой переменной, которую Вы хотите защитить, если Вы находитесь в критическом разделе затем, ничто иное не может прервать Вас.
Я думаю единственная вещь, которая может заменить переменные снаружи критического раздела, то, если процесс совместно использует страницу памяти с другим процессом (можно ли сделать это?) и другой процесс начинает изменять значения. Взаимные исключения также помогли бы здесь, названный взаимными исключениями совместно используются через процессы или только процессы того же имени?
ВОПРОС 3: Корректен мой анализ критических разделов, и это должно кодировать быть переписанным для использования взаимных исключений? Я взглянул на другие объекты синхронизации (семафоры и спин-блокировки), они лучше подходят здесь?
ВОПРОС 4: Где критические разделы/взаимные исключения/семафоры/спин-блокировки подходят лучше всего? Таким образом, какая проблема синхронизации должна они относиться. Существует ли значительная потеря производительности для выбора того по другому?
И в то время как мы находимся на нем, я считал, что спин-блокировки не должны использоваться в одножильной многопоточной среде, только многоядерной многопоточной среде. Так, ВОПРОС 5: эта несправедливость, или в противном случае почему это правильно?
Заранее спасибо за любые ответы :)
В VS2005 и более поздних версиях компилятор C ++ ограничивает доступ к этой переменной с помощью барьеров памяти, гарантируя, что переменная всегда полностью записываются / читаются в основную системную память перед ее использованием.
Совершенно верно. Если вы не создаете переносимый код, Visual Studio реализует его именно так. Если вы хотите быть портативным, ваши возможности в настоящее время «ограничены». До C ++ 0x не существует переносимого способа указания атомарных операций с гарантированным порядком чтения / записи, и вам необходимо реализовать решения для каждой платформы. Тем не менее, boost уже сделал за вас грязную работу, и вы можете использовать его атомарные примитивы .
Если вы все же будете выравнивать их, вы в безопасности. Если вы этого не сделаете, правила будут сложными (строки кэша, ...), поэтому самый безопасный способ - сохранить их выровненными, поскольку этого легко добиться.
Критическая секция - это облегченный мьютекс.Если вам не нужна синхронизация между процессами, используйте критические секции.
Критические секции могут даже выполнять за вас ожидания вращения .
Spin lock использует тот факт, что пока ожидающий CPU вращается, другой CPU может снять блокировку.Это не может произойти только с одним процессором, поэтому это просто пустая трата времени. На многопроцессорных системах спин-блокировки могут быть хорошей идеей, но это зависит от того, как часто ожидание вращения будет успешным. Идея ожидания в течение короткого времени намного быстрее, чем переключение контекста туда и обратно, поэтому, если ожидание, вероятно, будет коротким, лучше подождать.
Вопросы 3: CRITICAL_SECTIONs и Mutexes работают в значительной степени, так же. Мьютекс Win32 - это объект ядра, поэтому его можно использовать совместно с процессами и ожидать с помощью WaitForMultipleObjects, чего нельзя сделать с CRITICAL_SECTION. С другой стороны, CRITICAL_SECTION легче и, следовательно, быстрее. Но логика кода не должна зависеть от того, как вы его используете.
Вы также отметили, что «нет необходимости в критической секции для каждой переменной, которую вы хотите защитить, если вы находитесь в критической секции, тогда ничто другое не сможет вас прервать». Это правда, но компромисс заключается в том, что для доступа к любой из переменных вам потребуется удерживать эту блокировку. Если переменные можно осмысленно обновлять независимо друг от друга, вы теряете возможность распараллеливать эти операции. (Но поскольку они являются членами одного и того же объекта, я бы хорошенько подумал, прежде чем заключить, что к ним действительно можно получить доступ независимо друг от друга.)
Энергозависимость не подразумевает барьеров памяти.
Это означает только то, что это будет частью воспринимаемого состояния модели памяти. Следствием этого является то, что компилятор не может оптимизировать переменную и не может выполнять операции с переменной только в регистрах ЦП (фактически он загружается и сохраняется в памяти).
Поскольку не существует никаких препятствий для памяти, компилятор может изменять порядок инструкций по своему усмотрению. Единственная гарантия заключается в том, что порядок, в котором различные изменчивые переменные читаются / записываются, будет таким же, как в коде:
void test()
{
volatile int a;
volatile int b;
int c;
c = 1;
a = 5;
b = 3;
}
С помощью приведенного выше кода (при условии, что c
не оптимизирован) обновление до c
может произойти до или после обновлений a
и b
, обеспечивая 3 возможных результата. Обновления a
и b
гарантированно будут выполняться по порядку. c
может быть легко оптимизирован любым компилятором.Имея достаточно информации, компилятор может даже оптимизировать a
и b
(если можно доказать, что никакие другие потоки не читают переменные и что они не привязаны к аппаратному массиву (так в этом случае они фактически могут быть удалены). Обратите внимание, что стандарт не требует определенного поведения, а скорее воспринимаемого состояния с правилом как если бы
.
1: Volatile сам по себе практически бесполезен для многопоточности. Это гарантирует, что чтение / запись будет выполняться, а не сохранение значения в регистре, и гарантирует, что чтение / запись не будет переупорядочена относительно других энергозависимых
операций чтения / записи. .Но он все равно может быть переупорядочен по отношению к энергонезависимым, что в основном составляет 99,9% вашего кода. Microsoft переопределила volatile
, чтобы также заключать все обращения в барьеры памяти, но в целом это не гарантируется. Он просто незаметно сломается на любом компиляторе, который определяет volatile
, как это делает стандарт. (Код будет компилироваться и запускаться, он просто больше не будет потокобезопасным)
Кроме того, чтение / запись в объекты целого размера являются атомарными на x86, если объект хорошо выровнен. (У вас нет гарантии , когда запись все же произойдет. Компилятор и ЦП могут переупорядочить ее, чтобы она была атомарной, но не поточно-ориентированной)
2: Да, объект должен быть выровнен чтобы чтение / запись было атомарным.
3: Не совсем. Только один поток может выполнять код внутри данной критической секции одновременно. Другие потоки по-прежнему могут выполнять другой код. Таким образом, у вас может быть четыре переменных, каждая из которых защищена отдельным критическим разделом. Если бы все они использовали одну и ту же критическую секцию, я бы не смог манипулировать объектом 1, пока вы манипулируете объектом 2, что неэффективно и ограничивает параллелизм больше, чем необходимо. Если они защищены разными критическими секциями, мы просто не сможем одновременно манипулировать одним и тем же объектом.
4: Спин-блокировки редко являются хорошей идеей. Они полезны, если вы ожидаете, что поток будет ждать очень короткое время, прежде чем сможет получить блокировку, и вам абсолютно необходима минимальная задержка.Это позволяет избежать переключения контекста ОС, что является относительно медленной операцией. Вместо этого поток просто сидит в цикле, постоянно опрашивая переменную. Таким образом, более высокая загрузка ЦП (ядро не освобождается для запуска другого потока во время ожидания спин-блокировки), но поток сможет продолжить , как только будет снята блокировка.
Что касается остальных, характеристики производительности практически такие же: просто используйте ту, которая имеет семантику, наиболее подходящую для ваших нужд. Обычно критические секции наиболее удобны для защиты общих переменных, а мьютексы можно легко использовать для установки «флага», позволяющего продолжить работу других потоков.
Что касается отказа от использования спин-блокировок в одноядерной среде, помните, что спин-блокировки на самом деле не работают. Поток A, ожидающий спин-блокировки, на самом деле не приостанавливается, позволяя ОС планировать запуск потока B. Но поскольку A ожидает этой спин-блокировки, какой-то другой поток должен будет снять эту блокировку. Если у вас только одно ядро, то этот другой поток сможет работать только при отключении A. В нормальной ОС это рано или поздно произойдет в рамках обычного переключения контекста. Но поскольку мы знаем, что A не сможет получить блокировку, пока B не успеет выполнить и освободить блокировку, было бы лучше, если бы A просто уступил немедленно, был помещен в очередь ожидания ОС и перезапускается, когда B снимает блокировку. И это то, что делают все другие типы блокировок. Спин-блокировка по-прежнему будет работать в одноядерной среде (при условии, что ОС с вытесняющей многозадачностью), она будет очень неэффективной.
Не используйте volatile. Это не имеет практически никакого отношения к потокобезопасности. Смотрите здесь.
Присвоение BOOL не нуждается в примитивах синхронизации. Оно будет работать нормально без каких-либо специальных усилий с вашей стороны.
Если вы хотите установить переменную и затем убедиться, что другой поток увидит новое значение, вам нужно установить какую-то связь между двумя потоками. Простая блокировка непосредственно перед присвоением ничего не даст, потому что другой поток мог прийти и уйти до того, как вы получили блокировку.
И последнее слово предостережения: потоковую обработку очень трудно сделать правильно. Самые опытные программисты, как правило, наименее удобны в использовании потоков, что должно стать тревожным звоночком для любого, кто не имеет опыта их использования. Я настоятельно рекомендую вам использовать некоторые примитивы более высокого уровня для реализации параллелизма в вашем приложении. Передача неизменяемых структур данных через синхронизированные очереди - один из подходов, который существенно снижает опасность.
1) Нет volatile просто говорит, что перезагружать значение из памяти каждый раз, когда это ЕЩЕ ВОЗМОЖНО чтобы он был обновлен наполовину.
Изменить: 2) Windows предоставляет несколько элементарных функций. Найдите «Связанные» функции .
Комментарии побудили меня еще немного почитать. Если вы прочитали Руководство по системному программированию Intel , то увидите, что выровненное чтение и запись ЯВЛЯЮТСЯ атомарными.
8.1.1 Гарантированные атомарные операции
Процессор Intel486 (и более новые процессоры с тех пор) гарантирует, что следующие
базовые операции с памятью всегда будут выполняться атомарно:
• Чтение или запись byte
• Чтение или запись слова, выровненного по 16-битной границе
• Чтение или запись двойного слова, выровненного по 32-битной границе
{{1} } Процессор Pentium (и более новые процессоры с тех пор) гарантирует, что следующие
операции с дополнительной памятью всегда будут выполняться атомарно:
• Чтение или запись четверного слова, выровненного по 64-битной границе
• 16-битные обращения к некэшируемым ячейкам памяти, которые соответствуют 32-битной шине данных
Процессоры семейства P6 (и более новые процессоры с тех пор) гарантируют, что следующие {{ 1}} операция с дополнительной памятью всегда будет выполняться атомарно:
• Невыровненные 16-, 32- и 64-разрядные обращения к кэшированной памяти, которые помещаются в строку кэша
Доступ к кеш-памяти Для Intel Core 2 Duo, Intel
Atom, Intel Core Duo, Pentium M, Pentium 4 не гарантируется, что файловая память, разделенная по ширине шины, строкам кэша и
границам страниц, является атомарной. , Intel Xeon, семейство P6, Pentium и
процессоры Intel486. Процессоры семейства Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium M,
Pentium 4, Intel Xeon и P6 выдают сигналы управления шиной, которые
позволяют подсистемам внешней памяти делать разделенный доступ атомарным. ; однако
доступ к невыровненным данным серьезно повлияет на производительность процессора, и
следует избегать.
Инструкция x87 или инструкции SSE, которые обращаются к данным, размер которых превышает четверное слово
, могут быть реализованы с использованием множественных обращений к памяти. Если такая инструкция сохраняет
в памяти, некоторые обращения могут завершиться (запись в память), в то время как другой
вызывает сбой операции по архитектурным причинам (например, из-за записи в таблице страниц {{1 }} с пометкой «отсутствует»). В этом случае результаты выполненных доступов
могут быть видны программному обеспечению, даже если общая инструкция вызвала ошибку. Если аннулирование TLB
было отложено (см. Раздел 4.10.3.4), такие сбои страницы могут произойти
, даже если все обращения относятся к одной и той же странице.
В общем, да, если вы выполняете 8-битное чтение / запись с любого адреса, 16-битное чтение / запись с 16-битного выровненного адреса и т. Д., Вы получаете атомарные операции. Также интересно отметить, что вы можете выполнять чтение / запись невыровненной памяти в строке кэша на современной машине. Правила кажутся довольно сложными, поэтому на вашем месте я бы не стал на них полагаться. Приветствую комментаторов, для меня это хороший опыт обучения :)
3) Критическая секция несколько раз попытается запустить блокировку для своей блокировки, а затем заблокирует мьютекс. Блокировка вращения может потреблять мощность процессора, ничего не делая, а мьютекс может занять некоторое время, чтобы выполнить свою задачу. CriticalSections - хороший выбор, если вы не можете использовать взаимосвязанные функции.
4) За выбор одного из них накладываются штрафы. Это довольно большая просьба, чтобы узнать о преимуществах всего, что здесь есть. В справке MSDN есть много полезной информации по каждому из них. Я советую их прочитать.
5) Вы можете использовать спин-блокировку в однопоточной среде, это обычно не требуется, хотя управление потоками означает, что вы не можете иметь 2 процессора, обращающихся к одним и тем же данным одновременно. Это просто невозможно.