Альтернативы для замков для синхронизации - PullRequest
12 голосов
/ 26 ноября 2010

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

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

Ответы [ 4 ]

13 голосов
/ 26 ноября 2010

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

1 do
2 {
3     current_value = the_varibale
4     new_value = ...some expression using current_value...
5 } while(!compare_and_swap(the_variable, current_value, new_value));
  • compare_and_swap() атомарно проверяет, является ли значение the_variable по-прежнему current_value, и только если это так, оно будет обновлятьсяЗначение the_variable в new_value и возвращаемый true

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

Значение заключается в том, что когда другой поток обновляет переменную после чтения в строке 3, но до того, как CAS в строке 5 попытается обновить, инструкция сравнения и обмена будетошибка, потому что состояние, из которого вы обновляете, равно , а не , которое вы использовали для вычисления желаемого целевого состояния.Можно сказать, что такие циклы do / while «вращаются», а не блокируются, поскольку они вращаются и округляются до тех пор, пока CAS не преуспеет.

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

  • Первый этап: вращение с использованием CAS или аналогичного (т. е. вращение на {чтение текущего значения, если оно не установлено, затем cas (текущий)= не установлено, новое = установлено)}) - это означает, что другие потоки, выполняющие быстрое обновление, часто не приводят к тому, что ваш поток переключается на ожидание, и все связанные с этим относительно длительные накладные расходы связаны с этим.

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

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

Алгоритмы без блокировок вступают в свои права, когда вы работаете непосредственно с переменной, достаточно маленькой, чтобы обновляться непосредственно самой инструкцией CAS.Вместо того, чтобы ...

  • получить мьютекс (вращаясь на CAS, возвращаясь к более медленной очереди ОС)
  • обновить переменную
  • освободить мьютекс

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

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

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

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

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

2 голосов
/ 26 ноября 2010

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

Оба эти могут быть реализованы с использованием блокировок, но это деталь реализации.

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

1 голос
/ 26 ноября 2010

Еще одна библиотека для добавления в список чтения: Fast Flow

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

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

1 голос
/ 26 ноября 2010

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

Тем не менее, большинство решений без блокировок требуют значительных усилий со стороны человека, разрабатывающего конкретную программу / конкретную проблемную область. Они не подходят для всех проблем. Примеры таких реализаций приведены в библиотеке Intel Threading Building Blocks .

Самое важное отметить, что бесплатное решение без блокировки не является бесплатным. Вы собираетесь отказаться от чего-то, чтобы это работало, при минимальном уровне сложности реализации и, возможно, производительности в сценариях, где вы работаете на одном ядре (например, связанный список НАМНОГО медленнее, чем вектор). Убедитесь, что вы выполняете тестирование, прежде чем использовать блокировку без ограничений, исходя из предположения, что это будет быстрее.

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

...