Вопрос

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

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

На работе я пытался отладить какой-то многопоточный код, и я наткнулся на это:

EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);

Сейчас, m_bSomeVariable это Win32 BOOL (не volatile), который, насколько я знаю, определен как int, и на x86 чтение и запись этих значений является одной инструкцией, а поскольку переключение контекста происходит на границе инструкции, то нет необходимости синхронизировать эту операцию с критическим разделом.

Я провел еще несколько исследований в Интернете, чтобы выяснить, не нуждалась ли эта операция в синхронизации, и я придумал два сценария, по которым она выполнялась:

  1. Процессор выполняет выполнение не по порядку, или второй поток выполняется на другом ядре, и обновленное значение не записывается в оперативную память для просмотра другим ядром;и
  2. Значение int не выровнено по 4 байтам.

Я считаю, что номер 1 может быть решен с помощью ключевого слова "volatile".В VS2005 и более поздних версиях компилятор C ++ ограничивает доступ к этой переменной барьерами памяти, гарантируя, что переменная всегда полностью записывается / считывается в основную системную память перед ее использованием.

Номер 2 Я не могу проверить, я не знаю, почему выравнивание байтов будет иметь значение.Я не знаю набор инструкций x86, но делает mov нужно ли указывать 4-байтовый выровненный адрес?Если нет, нужно ли вам использовать комбинацию инструкций?Это привело бы к возникновению проблемы.

Итак...

ВОПРОС 1: Освобождает ли использование ключевого слова "volatile" (подразумевающее использование барьеров памяти и указание компилятору не оптимизировать этот код) программиста от необходимости синхронизировать 4-байтовую / 8-байтовую переменную на x86 / x64 между операциями чтения / записи?

ВОПРОС 2: Существует ли явное требование, чтобы переменная была выровнена по 4 байтам / 8 байтам?

Я еще немного покопался в нашем коде и переменных, определенных в классе:

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

И пока мы этим занимаемся, я прочитал, что spinlocks не следует использовать в одноядерной многопоточной среде, только в многоядерной многопоточной среде.Итак, ВОПРОС 5: Это неправильно, или если нет, то почему это правильно?

Заранее спасибо за любые ответы :)

Это было полезно?

Решение

Q1:Использование ключевого слова "volatile"

В VS2005 и более поздних версиях компилятор C ++ ограничивает доступ к этой переменной барьерами памяти, гарантируя, что переменная всегда полностью записывается / считывается в основную системную память перед ее использованием.

Вот именно.Если вы не создаете переносимый код, Visual Studio реализует его именно таким образом.Если вы хотите быть переносимым, ваши возможности в настоящее время "ограничены".До C ++ 0x не существовало переносимого способа указания атомарных операций с гарантированным порядком чтения / записи, и вам необходимо внедрять решения для каждой платформы.Тем не менее, boost уже выполнил за вас грязную работу, и вы можете использовать его атомарные примитивы.

Q2:Переменная должна быть выровнена по 4 байтам / 8 байтам?

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

Q3:Должен ли этот код быть переписан для использования мьютексов?

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

Q4:Где лучше всего подходят критические разделы / мьютексы / семафоры / спин-блокировки?

Критические разделы может даже do spin ожидает для тебя.

Q5:Spinlocks не следует использовать в одноядерном

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

Другие советы

1) No volatile просто говорит о повторной загрузке значения из памяти каждый раз, когда оно ВСЕ еще может быть обновлено наполовину.

Редактировать:2) Windows предоставляет некоторые атомарные функции.Посмотрите на "Взаимосвязанные" функции.

Комментарии побудили меня еще немного почитать.Если вы прочтете до конца Руководство по системному программированию Intel Вы можете видеть, что выровненное чтение и запись являются атомарными.

8.1.1 Гарантированные атомарные операции Процессор Intel486 (и более новые процессоры с тех пор) гарантирует, что следующие основные операции с памятью всегда будут выполняться атомарно:
• Чтение или запись байта
• Чтение или запись слова, выровненного по 16-битной границе
• Чтение или запись двойного слова, выровненного по 32-разрядной границе
Процессор Pentium (и более новые процессоры с тех пор) гарантирует, что следующее дополнительные операции с памятью всегда будут выполняться атомарно:
• Чтение или запись четырехсловия, выровненного по 64-разрядной границе
• 16-разрядный доступ к некэшированным ячейкам памяти, которые помещаются в 32-разрядную шину данных
Процессоры семейства P6 (и более новые процессоры с тех пор) гарантируют, что следующее дополнительная операция с памятью всегда будет выполняться атомарно:
• Невыровненные 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, которые обращаются к данным размером больше четырехсловия могут быть реализованы с использованием множественных обращений к памяти.Если такая инструкция сохраняет в памяти, некоторые обращения могут завершиться (запись в память), в то время как другая приводит к сбою операции по архитектурным причинам (напримериз-за записи в таблице страниц которая помечена как “отсутствует”).В этом случае последствия завершенных обращений могут быть видны программному обеспечению, даже если общая инструкция вызвала сбой.Если TLB аннулирование было отложено (см. Раздел 4.10.3.4), могут возникать такие ошибки страницы даже если все обращения осуществляются к одной и той же странице.

Так что в принципе да, если вы выполняете 8-битное чтение / запись с любого адреса, 16-битное чтение / запись с 16-битного выровненного адреса и т.д. и т.д., вы получаете атомарные операции.Также интересно отметить, что вы можете выполнять невыровненное чтение / запись в память в пределах строки кэша на современном компьютере.Однако правила кажутся довольно сложными, поэтому на вашем месте я бы не полагался на них.Приветствую комментаторов, для меня это хороший опыт обучения :)

3) Критический раздел попытается несколько раз запустить блокировку для своей блокировки, а затем заблокирует мьютекс.Блокировка вращения может высасывать мощность процессора, ничего не делая, и мьютексу может потребоваться некоторое время, чтобы выполнить свою работу.Критические разделы - хороший выбор, если вы не можете использовать взаимосвязанные функции.

4) Существуют ограничения производительности за выбор одного вместо другого.Это довольно большая просьба - рассказать о преимуществах всего, что здесь есть.Справка MSDN содержит много полезной информации по каждому из них.Я предлагаю их прочесть.

5) Вы можете использовать блокировку вращения в однопоточной среде, хотя обычно в этом нет необходимости, поскольку управление потоками означает, что у вас не может быть одновременного доступа 2 процессоров к одним и тем же данным.Это просто невозможно.

1:Изменчивый сам по себе практически бесполезен для многопоточности.Это гарантирует, что чтение / запись будут выполнены, а не сохранение значения в регистре, и это гарантирует, что чтение / запись не будут переупорядочены в отношении других volatile считывает / записывает.Но он все еще может быть переупорядочен в отношении энергонезависимых, что в основном составляет 99,9% вашего кода.Microsoft переопределила volatile также обернуть все обращения в барьеры памяти, но в целом это не гарантируется.Он просто беззвучно прервется на любом компиляторе, который определяет volatile как это делает стандарт.(Код будет скомпилирован и запущен, он просто больше не будет потокобезопасным)

Кроме того, чтение / запись в объекты целочисленного размера являются атомарными на x86 до тех пор, пока объект хорошо выровнен.(У вас нет никаких гарантий когда тем не менее, запись произойдет.Компилятор и центральный процессор могут изменить его порядок, поэтому он является атомарным, но не потокобезопасным)

2:Да, объект должен быть выровнен, чтобы чтение / запись были атомарными.

3:Не совсем.Только один поток может выполнять код внутри заданной критической секции за раз.Другие потоки все еще могут выполнять другой код.Таким образом, у вас может быть четыре переменные, каждая из которых защищена отдельной критической секцией.Если бы все они совместно использовали один и тот же критический раздел, я не смог бы манипулировать объектом 1, пока вы манипулируете объектом 2, что неэффективно и ограничивает параллелизм больше, чем необходимо.Если они защищены разными критическими разделами, мы просто не можем одновременно манипулировать то же самое объект одновременно.

4:Спинлоки - это редко хорошая идея.Они полезны, если вы ожидаете, что потоку придется ждать очень короткое время, прежде чем он сможет получить блокировку, и вам абсолютно необходима минимальная задержка.Это позволяет избежать переключения контекста операционной системы, что является относительно медленной операцией.Вместо этого поток просто находится в цикле, постоянно опрашивая переменную.Таким образом, более высокая загрузка процессора (ядро не освобождается для запуска другого потока в ожидании блокировки вращения), но поток сможет продолжить как можно скорее как только блокировка будет снята.

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

Что касается отказа от использования spinlocks в одноядерной среде, помните, что spinlock на самом деле не дает результатов.Поток A, ожидающий блокировки вращения, на самом деле не переводится в режим ожидания, позволяя операционной системе запланировать запуск потока B.Но поскольку A ожидает эту блокировку вращения, какому-то другому потоку придется снять эту блокировку.Если у вас есть только одно ядро, то этот другой поток сможет запускаться только при отключении A.При нормальной операционной системе это все равно рано или поздно произойдет как часть обычного переключения контекста.Но поскольку мы знаем, что A не сможет получить блокировку до тех пор, пока у B не будет времени для выполнения и освобождение блокировка, нам было бы лучше, если бы A просто немедленно уступила, была помещена операционной системой в очередь ожидания и перезапущена, когда B снимет блокировку.И это то, что все Другое типы замков так и делают.Спин-замок все равно будет работа в одноядерной среде (предполагая ОС с преимущественной многозадачностью) это будет просто очень, очень неэффективно.

Не используйте volatile.Это практически не имеет никакого отношения к потокобезопасности.Видишь здесь для тех, кто находится в самом низу.

Присвоение BOOL не требует никаких примитивов синхронизации.Это будет работать нормально без каких-либо особых усилий с вашей стороны.

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

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

Volatile не подразумевает барьеров для памяти.

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

Поскольку никаких подразумеваемых барьеров памяти не существует, компилятор может изменять порядок команд по своему желанию.Единственная гарантия заключается в том, что порядок чтения / записи различных изменчивых переменных будет таким же, как в коде:

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 (если можно доказать, что никакие другие потоки не считывают переменные и что они не привязаны к аппаратному массиву (так что в этом случае их действительно можно удалить).Обратите внимание, что стандарт не требует определенного поведения, а скорее воспринимаемого состояния с as-if правило.

Вопросы 3:CRITICAL_SECTIONs и Мьютексы работают, в значительной степени, одинаково.Мьютекс Win32 - это объект ядра, поэтому его можно совместно использовать между процессами и ожидать с помощью WaitForMultipleObjects, чего нельзя сделать с помощью CRITICAL_SECTION .С другой стороны, CRITICAL_SECTION имеет меньший вес и, следовательно, быстрее.Но логика кода, который вы используете, не должна зависеть от того, что вы используете.

Вы также прокомментировали, что "нет необходимости в критическом разделе для каждой переменной, которую вы хотите защитить, если вы находитесь в критическом разделе, то ничто другое не может вам помешать". Это верно, но компромисс заключается в том, что для доступа к любой из переменных потребуется, чтобы вы удерживали эту блокировку.Если переменные могут осмысленно обновляться независимо, вы теряете возможность распараллеливания этих операций.(Однако, поскольку это члены одного и того же объекта, я бы хорошенько подумал, прежде чем прийти к выводу, что к ним действительно можно получить доступ независимо друг от друга.)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top