multithreading - thread - Насколько эффективна блокировка разблокированных мьютексов? Какова стоимость мьютекса?




thread c++ (3)

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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 на моем ящике). Третий asm представляет собой цикл задержки. Размер цикла задержки равен 1 счету меньше для потока 1, чем для потока 0, поэтому поток 1 немного быстрее.

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

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

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

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

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





blocking