Насколько эффективна блокировка разблокированного мьютекса? Сколько стоит мьютекс?

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

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

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

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

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


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


person Albert    schedule 06.09.2010    source источник
comment
Это будет зависеть от реализации и архитектуры. Некоторые мьютексы почти ничего не будут стоить при наличии встроенной аппаратной поддержки, другие будут стоить очень дорого. Без дополнительной информации ответить невозможно.   -  person Gian    schedule 06.09.2010
comment
@Gian: Ну, конечно, я подразумеваю этот подвопрос в своем вопросе. Я хотел бы знать об общем оборудовании, но также о заметных исключениях, если таковые имеются.   -  person Albert    schedule 06.09.2010
comment
Я действительно нигде не вижу такого подтекста. Вы спрашиваете об инструкциях ассемблера - ответ может быть от 1 до десяти тысяч инструкций, в зависимости от того, о какой архитектуре вы говорите.   -  person Gian    schedule 06.09.2010
comment
@Gian: Тогда дайте, пожалуйста, именно такой ответ. Пожалуйста, скажите, что это на самом деле на x86 и amd64, пожалуйста, дайте пример архитектуры, где это 1 инструкция, и укажите, где это 10k. Разве не ясно, что я хочу знать это из своего вопроса?   -  person Albert    schedule 07.09.2010


Ответы (5)


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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

person Dummy00001    schedule 06.09.2010
comment
Спасибо, это в основном отвечает на мой вопрос. Я не знал, что ядро ​​(например, ядро ​​Linux) обрабатывает мьютексы, и вы управляете ими с помощью системных вызовов. Но поскольку Linux сам управляет планированием и переключением контекста, это имеет смысл. Но теперь у меня есть приблизительное представление о том, что блокировка / разблокировка мьютекса будет делать внутри. - person Albert; 07.09.2010
comment
@ Альберт: Ой. Я забыл переключение контекста ... Переключение контекста слишком сильно сказывается на производительности. Если получение блокировки не удается и поток должен ждать, это слишком похоже на половину переключения контекста. Сам по себе CS работает быстро, но поскольку ЦП может использоваться каким-либо другим процессом, кеши будут заполнены чужеродными данными. После того, как поток, наконец, получит блокировку, высока вероятность того, что ЦП придется заново перезагружать почти все из ОЗУ. - person Dummy00001; 07.09.2010
comment
@ Dummy00001 Переключение на другой процесс означает, что вам нужно изменить отображение памяти ЦП. Это не так уж и дешево. - person curiousguy; 09.12.2019

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

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

Программу, которую я использовал для исследования этого вопроса, можно найти здесь:

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

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

введите описание изображения здесь

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

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 немного быстрее.

Вышеупомянутая функция вызывается в узком цикле размером 100000. Несмотря на то, что функция немного быстрее для потока 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 реализован таким образом, что он запускает блокировки на некоторое время, прежде чем фактически выполнять системные вызовы, когда не удается немедленно получить блокировку на мьютексе (что, несомненно, будет очень медленным). Итак, то, что я здесь измерил, является наиболее идеальной ситуацией, и на практике блокировка и разблокировка могут занимать значительно больше времени на блокировку / разблокировку.

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

person Carlo Wood    schedule 07.04.2018

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

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

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

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

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

person valdo    schedule 06.09.2010
comment
Критические разделы Windows гораздо ближе к мьютексам. У них обычная семантика мьютексов, но они локальны для процесса. Последняя часть делает их намного быстрее, так как они могут обрабатываться полностью внутри вашего процесса (и, следовательно, кода пользовательского режима). - person MSalters; 06.09.2010
comment
Число было бы более полезным, если бы количество циклов ЦП общих операций (например, арифметических / if-else / cache-miss / косвенного обращения) также было предоставлено для сравнения. .... Было бы даже здорово, если бы там были какие-то упоминания номера. В интернете такую ​​информацию найти очень сложно. - person javaLover; 14.05.2017
comment
@javaLover Операции не выполняются циклически; они выполняются на арифметических устройствах в течение ряда циклов. Это совсем другое. Стоимость любой инструкции во времени - это не определенная величина, а только стоимость использования ресурсов. Эти ресурсы являются общими. Влияние инструкций памяти во многом зависит от кеширования и т. Д. - person curiousguy; 03.12.2019
comment
@curiousguy Согласен. Мне было непонятно. Я хотел бы ответить, например, std::mutex в среднем используют продолжительность (в секундах) в 10 раз больше, чем int++. Однако я знаю, что на это сложно ответить, потому что это во многом зависит от многих вещей. - person javaLover; 11.03.2020

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

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

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

person Grant Petty    schedule 18.11.2018

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

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

В однопроцессорных системах вы можете просто отключить прерывания на время, достаточное для атомарного изменения данных. Многопроцессорные системы могут использовать стратегию «тестируй и устанавливай».

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

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

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

person paxdiablo    schedule 06.09.2010
comment
Да, но насколько эффективно? Это единая машинная инструкция? Или около 10? Или около 100? 1000? Более? Все это по-прежнему эффективно, но может иметь значение в экстремальных ситуациях. - person Albert; 06.09.2010
comment
Что ж, это полностью зависит от реализации. Вы можете отключить прерывания, проверить / установить целое число и повторно активировать прерывания в цикле примерно за шесть машинных инструкций. Test-and-set может быть выполнено примерно в любом количестве, поскольку процессоры, как правило, предоставляют это в виде одной инструкции. - person paxdiablo; 06.09.2010
comment
Тестирование и установка с синхронизацией по шине - это одна (довольно длинная) инструкция на x86. Остальная часть оборудования для его использования работает довольно быстро («тест прошел успешно?» - вопрос, в котором процессоры умеют быстро справляться), но действительно имеет значение длина инструкции с блокировкой шины, поскольку это часть, которая блокирует что-либо. Решения с прерываниями работают намного медленнее, потому что манипулирование ими обычно ограничивается ядром ОС, чтобы остановить тривиальные DoS-атаки. - person Donal Fellows; 06.09.2010
comment
Кстати, не используйте drop / reacquire как средство передачи потока другим; это отстойная стратегия для многоядерной системы. (Это одна из немногих вещей, в которых CPython ошибается.) - person Donal Fellows; 06.09.2010
comment
@Donal: Что вы имеете в виду под "сбросить / получить"? Звучит важно; можешь дать мне больше информации по этому поводу? - person Albert; 07.09.2010
comment
@paxdiablo: Кстати, да, мне нужен мьютекс, но у меня есть выбор между использованием около 100 (или более) из них или одного. - person Albert; 07.09.2010
comment
@Albert: Нет. Вы только попытаетесь реализовать это, и ваш код будет отстойным, и вы этого не заметите. Почему бы не поступить разумно и не перестать беспокоиться об этой мелочи? Просто используйте правильные шаблоны параллелизма, которые уже существуют, и избавьте себя от множества неприятностей. И перестаньте беспокоиться о том, как реализованы блокировки; беспокоиться о том, как они работают на практике по результатам тестирования. - person Donal Fellows; 07.09.2010
comment
@Donal: Я не имел в виду, что хочу его использовать. Я просто хочу знать, что вы имеете в виду, чтобы убедиться, что я не использую его, и что я могу понять, почему использовать его - плохая идея. Я в основном просил ссылки на это, которые дают некоторую предысторию / подробности об этом. - person Albert; 07.09.2010
comment
@ Альберт: Ах да. Дэвид Бизли подробно рассказал об этом. dabeaz.com/GIL и blip.tv/file/2232410 должно быть актуальным. - person Donal Fellows; 07.09.2010
comment
В некоторых случаях это не нужно. Вы также можете получить локальную копию вместо совместного использования общего ресурса. - person curiousguy; 03.12.2019