Подтвердить что ты не робот

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

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

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

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

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

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

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

  • ЦП реализует не исполнение заказа или второй поток работает на другом ядре, а обновленное значение не записывается в ОЗУ для другого ядра; и
  • int не выравнивается по 4 байт.

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

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

Итак...

ВОПРОС 1: Использует ли ключевое слово "volatile" (неявное использование барьеров памяти и намекает на компилятор, чтобы не оптимизировать этот код) освобождает программиста от необходимости синхронизировать 4-байтовый/8 -byte на переменную 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: Является ли это неправильным, а если нет, то почему это правильно?

Заранее благодарим за любые ответы:)

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

8.1.1 Гарантированные атомные операции Процессор Intel486 (и более новые процессоры с тех пор) гарантирует, что следующее операции базовой памяти всегда будут выполняться атомарно:
• Чтение или запись байта
• Чтение или запись слова, выровненного на 16-битной границе
• Чтение или запись двойного слова, выровненного на 32-битной границе
Процессор Pentium (и более новые процессоры с тех пор) гарантирует, что следующее дополнительные операции с памятью всегда будут выполняться атомарно:
• Чтение или запись квадлового слова, выровненного на 64-битной границе
• 16-разрядный доступ к нераспакованным ячейкам памяти, которые подходят к 32-разрядной шине данных

Процессоры семейства P6 (и более новые процессоры с тех пор) гарантируют, что следующие дополнительная операция памяти всегда будет выполняться атомарно:
• Unaligned 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) Критический раздел попытается несколько раз заблокировать замок для его блокировки, а затем заблокирует мьютекс. Spin Locking может сосать мощность процессора, ничего не делая, и мьютекс может занять некоторое время, чтобы сделать свой материал. CriticalSections - хороший выбор, если вы не можете использовать блокированные функции.

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

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

Ответ 3

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

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

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

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

Летучие не подразумевают барьеров памяти.

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

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

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.

Ответ 6

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

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