Насколько эффективна блокировка разблокированного мьютекса? Какова стоимость мьютекса? - PullRequest
129 голосов
/ 06 сентября 2010

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

Насколько эффективно блокировать мьютекс? То есть сколько там ассемблерных инструкций и сколько времени они занимают (в случае, если мьютекс разблокирован)?

Сколько стоит мьютекс? Является ли проблемой иметь действительно много мьютексов? Или я могу просто добавить столько переменных мьютекса в мой код, сколько у меня int переменных, и это не имеет значения?

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

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


Сообщение в блоге WebKits (2016) о блокировке очень связано с этим вопросом и объясняет различия между спин-блокировкой, адаптивной блокировкой, фьютексом и т. Д.

Ответы [ 5 ]

99 голосов
/ 06 сентября 2010

У меня есть выбор между наличием нескольких мьютексов или одного для объекта.

Если у вас много потоков и доступ к объекту происходит часто, тогдамножественные блокировки увеличат параллелизм.За счет ремонтопригодности, поскольку большее количество блокировок означает больше отладки блокировок.

Насколько эффективно блокировать мьютекс?Т.е. сколько там инструкций на ассемблере, вероятно, и сколько времени они занимают (в случае, если мьютекс разблокирован)?

Точные инструкции на ассемблере требуют меньше всего мьютекса - когерентность памяти / кэша гарантии являются главными издержками.И реже берется конкретная блокировка - лучше.

Мьютекс состоит из двух основных частей (упрощение): (1) флаг, указывающий, заблокирован ли мьютекс или нет (2) очередь ожидания.

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

Блокировка разблокированного мьютекса действительно дешева.Разблокировка мьютекса без конфликтов тоже дешевая.

Сколько стоит мьютекс?Действительно ли проблема иметь много мьютексов?Или я могу просто выбросить столько мьютекс-переменных в моем коде, сколько у меня есть переменных типа int, и это на самом деле не имеет значения?

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

Сводка.Блокировки пользовательского пространства (и мьютексы в частности) дешевы и не подвержены каким-либо системным ограничениям.Но слишком много из них заклинаний кошмара для отладки.Простая таблица:

  1. Меньше блокировок означает больше конфликтов (медленные системные вызовы, задержки ЦП) и меньший параллелизм
  2. Меньше блокировок означает меньше проблем при отладке проблем многопоточности.
  3. Чем больше блокировок, тем меньше конфликтов и больше параллелизма
  4. Чем больше блокировок, тем больше шансов столкнуться с неразрушаемыми тупиками.

Необходимо найти и поддерживать сбалансированную схему блокировки для приложения, как правило, с балансировкой #2 и # 3.


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

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

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

15 голосов
/ 08 апреля 2018

Я хотел знать то же самое, поэтому я измерил это.На моем компьютере (8-ядерный процессор AMD FX (tm) -8150 с тактовой частотой 3,612361 ГГц) блокировка и разблокировка разблокированного мьютекса, который находится в собственной строке кэша и уже кэширован, занимает 47 часов (13 нс).

Из-за синхронизации между двумя ядрами (я использовал CPU # 0 и # 1), я мог вызывать пару блокировать / разблокировать только один раз каждые 102 нс в двух потоках, то есть один раз каждые 51 нс, из чего можно сделать вывод, что это занимаетпримерно 38 нс для восстановления после того, как поток выполнит разблокировку, прежде чем следующий поток сможет снова его заблокировать.

Программа, которую я использовал для исследования этого, может быть найдена здесь: https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx

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

График, который он генерирует в этом состоянии:

enter image description here

Показывает результаты прогонов теста для следующего кода:

uint64_t do_Ndec(int thread, int loop_count)
{
  uint64_t start;
  uint64_t end;
  int __d0;

  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
  mutex.lock();
  mutex.unlock();
  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
  asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
  return end - start;
}

Два вызова rdtsc измеряют количество часовчто нужно, чтобынажмите и разблокируйте `mutex '(с накладными расходами в 39 часов для вызовов rdtsc на моем ящике).Третий ассм - это петля задержки.Размер цикла задержки на 1 счет меньше для потока 1, чем для потока 0, поэтому поток 1 немного быстрее.

Вышеприведенная функция вызывается в узком цикле размером 100 000.Несмотря на то, что функция немного быстрее для потока 1, оба цикла синхронизируются из-за вызова мьютекса.Это видно на графике из того факта, что количество тактов, измеренных для пары блокировка / разблокировка, немного больше для потока 1, чтобы учесть более короткую задержку в цикле под ним.

На приведенном выше графикенижняя правая точка - это измерение с задержкой loop_count, равной 150, а затем, следуя точкам внизу, влево, loop_count уменьшается на единицу каждое измерение.Когда она становится 77, функция вызывается каждые 102 нс в обоих потоках.Если впоследствии loop_count уменьшается еще больше, то больше невозможно синхронизировать потоки, и мьютекс начинает фактически блокироваться большую часть времени, что приводит к увеличению количества часов, которое требуется для блокировки / разблокировки.Также из-за этого увеличивается среднее время вызова функции;поэтому точки заговора теперь снова идут вверх и вправо.

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

В целомя пришел к выводу, что ответ на вопрос OP заключается в том, что добавление большего числа мьютексов лучше, если это приводит к меньшему количеству конфликтов.

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

РЕДАКТИРОВАТЬ: я получил гораздо больше знаний по этому вопросу сейчас и начинаю сомневаться в заключении, которое я представил здесь.Прежде всего, CPU 0 и 1 оказываются гиперпоточными;Несмотря на то, что AMD утверждает, что имеет 8 реальных ядер, безусловно, есть что-то очень сомнительное, потому что задержки между двумя другими ядрами намного больше (то есть 0 и 1 образуют пару, как и 2 и 3, 4 и 5, и 6 и 7).Во-вторых, std :: mutex реализован таким образом, что он немного вращает блокировки перед тем, как фактически выполнять системные вызовы, когда не удается немедленно получить блокировку для мьютекса (что, без сомнения, будет чрезвычайно медленным).Итак, что я измерил здесь, так это абсолютную наиболее идеальную ситуацию, и на практике блокировка и разблокировка могут занять значительно больше времени на блокировку / разблокировку.

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

10 голосов
/ 06 сентября 2010

Это зависит от того, что вы на самом деле называете "мьютексом", режимом ОС и т. Д.

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

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

Например, на процессоре Intel Core Duo, Windows XP. Операция с блокировкой: занимает около 40 тактов процессора. Вызов режима ядра (то есть системный вызов) - около 2000 циклов ЦП.

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

6 голосов
/ 06 сентября 2010

Стоимость будет варьироваться в зависимости от реализации, но вы должны иметь в виду две вещи:

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

В однопроцессорных системах вы можете просто отключить прерывания на достаточно длительное время для атомарного изменения данных. Многопроцессорные системы могут использовать стратегию test-and-set .

В обоих случаях инструкции относительно эффективны.

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

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

1 голос
/ 18 ноября 2018

Я совершенно новичок в pthreads и mutex, но я могу подтвердить из экспериментов, что стоимость блокировки / разблокировки мьютекса почти равна нулю, когда нет конкуренции, но когда есть конфликты, цена блокировки чрезвычайно высока , Я запустил простой код с пулом потоков, в котором задачей было просто вычислить сумму в глобальной переменной, защищенной блокировкой мьютекса:

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

С одним потоком программа суммирует 10 000 000 значений практически мгновенно (менее одной секунды); с двумя потоками (на MacBook с 4 ядрами) одна и та же программа занимает 39 секунд.

...