Синхронизация потока 101

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

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

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

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

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

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

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

Я полагаю, что номер 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: эта несправедливость, или в противном случае почему это правильно?

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

18
задан AshleysBrain 31 March 2010 в 10:55
поделиться

6 ответов

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

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

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

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

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

Q3: Следует ли переписать этот код для использования мьютексов?

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

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

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

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

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

7
ответ дан 30 November 2019 в 07:18
поделиться

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

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

2
ответ дан 30 November 2019 в 07:18
поделиться

Энергозависимость не подразумевает барьеров памяти.

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

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

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

3
ответ дан 30 November 2019 в 07:18
поделиться

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

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

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

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

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

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

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

8
ответ дан 30 November 2019 в 07:18
поделиться

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

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

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

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

5
ответ дан 30 November 2019 в 07:18
поделиться

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 процессора, обращающихся к одним и тем же данным одновременно. Это просто невозможно.

13
ответ дан 30 November 2019 в 07:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: