Насколько дорого стоит заявление о блокировке?

111

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

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

Но мне было интересно ... насколько дорого обходится блокировка переменной? Какое негативное влияние на производительность?

Кис К. Баккер
источник
10
Блокировка переменной не так уж и дорога; это ожидание заблокированной переменной, которого вы хотите избежать.
Гейб
53
это намного дешевле, чем тратить часы на отслеживание другого состояния гонки ;-)
BrokenGlass
2
Что ж ... если блокировка стоит дорого, вы можете захотеть избежать ее, изменив программу так, чтобы для нее требовалось меньше блокировок. Мог бы реализовать какую-то синхронизацию.
Kees C. Bakker
1
У меня было резкое улучшение производительности (прямо сейчас, после прочтения комментария @Gabe), просто переместив много кода из моих блоков блокировки. Итог: с этого момента я оставлю только доступ к переменной (обычно одну строку) внутри блока блокировки, своего рода «своевременную блокировку». Имеет ли это смысл?
heltonbiker
2
@heltonbiker Конечно, в этом есть смысл. Это также должно быть архитектурным принципом, вы должны делать замки как можно короче, проще и быстрее. Только действительно необходимые данные, которые нужно синхронизировать. На серверных ящиках также следует учитывать гибридный характер блокировки. Разногласия, даже если они не критичны для вашего кода, происходят из-за гибридной природы блокировки, заставляющей ядра вращаться во время каждого доступа, если блокировка удерживается кем-то другим. Вы фактически потребляете некоторые ресурсы процессора из других служб на сервере в течение некоторого времени, прежде чем ваш поток будет приостановлен.
ipavlu

Ответы:

86

Вот статья, в которой рассказывается о стоимости. Короткий ответ - 50 нс.

Джейк Пирсон
источник
39
Краткий лучший ответ: 50 нс + время ожидания, если другой поток удерживает блокировку.
Herman
4
Чем больше потоков входит и выходит из блокировки, тем дороже это становится. Стоимость растет в геометрической прогрессии с увеличением количества потоков
Арсен Захрай, 06
16
Некоторый контекст: разделение двух чисел на 3Ghz x86 занимает около 10 нс (без учета времени, необходимого для выборки / декодирования инструкции) ; а загрузка одной переменной из (не кэшированной) памяти в регистр занимает около 40 нс. Итак, 50ns - это безумно, ослепительно быстро - вам не следует беспокоиться о стоимости использования lockбольше, чем о стоимости использования переменной.
BlueRaja - Дэнни Пфлугофт,
3
Кроме того, эта статья была старой, когда был задан этот вопрос.
Отис
3
Действительно отличная метрика, «почти бесплатно», не говоря уже о неверной. Вы, ребята, не учитываете, что он короткий и быстрый, и ТОЛЬКО если нет разногласий вообще, один поток. В ТАКОМ СЛУЧАЕ БЛОКИРОВКА НЕ НУЖНА. Вторая проблема, блокировка - это не блокировка, а гибридная блокировка, она обнаруживает внутри CLR, что блокировка никем не удерживается на основе атомарных операций, и в этом случае она избегает вызовов ядра операционной системы, то есть другого кольца, которое не измеряется этими тесты. То, что измеряется от 25 до 50 нс, на самом деле является кодом инструкций с блокировкой на уровне приложения, если блокировка не взята
ipavlu
50

Технический ответ заключается в том, что это невозможно количественно оценить, это сильно зависит от состояния буферов обратной записи памяти ЦП и от того, сколько данных, собранных устройством предварительной выборки, необходимо отбросить и перечитать. Оба они очень недетерминированы. Я использую 150 циклов процессора в качестве приблизительного приближения, которое позволяет избежать серьезных разочарований.

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

Чтобы получить точное число, вам нужно будет измерить. В Visual Studio есть удобный анализатор параллелизма, доступный как расширение.

Ганс Пассан
источник
1
На самом деле нет, это можно количественно определить и измерить. Это не так просто, как написать эти блокировки по всему коду, а затем заявить, что это всего лишь 50 нс, миф, измеряемый однопоточным доступом к замку.
ipavlu 06
8
«Думаю, ты можешь пропустить блокировку» ... Я думаю, что многие люди, читая этот вопрос, находятся именно в этом месте ...
Snoop
30

Дальнейшее чтение:

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

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https: // www. codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

Оригинальный ответ:

О, Боже!

Кажется, что правильный ответ, отмеченный здесь как ОТВЕТ, по своей сути неверен! С уважением прошу автора ответа прочитать связанную статью до конца. статья

Автор статьи из 2003 статьи измерял только на Dual Core машины и в первом измерительном случае он измеряется замок только с одним потоком , и результат был примерно 50ns за доступ замка.

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

Итак, автор говорит, что с двумя потоками на Dual Core блокировки стоят 120 нс, а с 3 потоками - до 180 нс. Таким образом, это явно зависит от количества потоков, одновременно обращающихся к блокировке.

Это просто, это не 50 нс, если только это не один поток, где блокировка становится бесполезной.

Другой вопрос, требующий внимания, - это то, что оно измеряется как среднее время !

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

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

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

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

ipavlu
источник
1
Чтобы уточнить, в статье не говорится, что производительность блокировки ухудшается с увеличением количества потоков в приложении; производительность ухудшается из-за количества потоков, борющихся за блокировку. (Это подразумевается, но не указано четко в ответе выше.)
Gooseberry
Полагаю, вы имеете в виду следующее: «Так что, похоже, это явно зависит от количества потоков, к которым одновременно осуществляется доступ, и чем больше, тем хуже». Да, формулировка могла быть лучше. Я имел в виду «с одновременным доступом», когда потоки одновременно обращались к блокировке, создавая таким образом конкуренцию.
ipavlu
20

Это не отвечает на ваш вопрос о производительности, но я могу сказать, что .NET Framework действительно предлагает Interlocked.Addметод, который позволит вам добавить amountваш doneчлен в свой член без ручной блокировки другого объекта.

Адам Марас
источник
1
Да, наверное, лучший ответ. Но в основном по причине более короткого и чистого кода. Разница в скорости вряд ли будет заметна.
Хенк Холтерман,
спасибо за этот ответ. Я больше занимаюсь замками. Добавленные ints - одни из многих. Мне нравится это предложение, с этого момента буду им пользоваться.
Kees C. Bakker
блокировки намного легче получить правильно, даже если код без блокировки потенциально быстрее. Interlocked.Add сам по себе имеет те же проблемы, что и + = без синхронизации.
ангар
10

lock (Monitor.Enter / Exit) очень дешево, дешевле, чем альтернативы, такие как Waithandle или Mutex.

Но что, если бы она была (немного) медленной, вы бы предпочли быструю программу с неверными результатами?

Хенк Холтерман
источник
5
Ха-ха ... Я хотел быструю программу и хорошие результаты.
Kees C. Bakker
@ henk-holterman Есть несколько проблем с вашими утверждениями: во- первых, как ясно показали этот вопрос и ответы, существует низкое понимание влияния блокировки на общую производительность, даже люди заявляют миф о 50 нс, который применим только в однопоточной среде. Во-вторых, ваше утверждение здесь и будет оставаться в течение многих лет, а тем временем количество ядер процессоров выросло, но скорость ядер не так сильно. ** Три ** приложения со временем становятся только более сложными, а затем слой за слоем блокировка в среде многих ядер, и их число растет, 2,4,8,10,20,16,32
ipavlu
Мой обычный подход - строить синхронизацию слабосвязанным способом с минимальным взаимодействием. Это происходит очень быстро для структур данных без блокировки. Я сделал свои обертки кода вокруг спин-блокировки, чтобы упростить разработку, и даже когда у TPL есть специальные параллельные коллекции, я разработал свои собственные коллекции с блокировкой вращения вокруг списка, массива, словаря и очереди, так как мне нужно было немного больше контроля, а иногда и некоторый код, работающий под спин-блокировка. Я могу вам сказать, что это возможно и позволяет решить несколько сценариев, которые коллекции TPL не могут выполнять, и с большим приростом производительности / пропускной способности.
ipavlu
7

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

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            {
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: {0}", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: {0}", stopwatch.ElapsedMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

Вывод:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208
Йохан Нильссон
источник
4
Это может быть плохой пример, потому что ваш цикл на самом деле ничего не делает, кроме назначения одной переменной, а блокировка - это как минимум 2 вызова функций. Кроме того, 20 нс на блокировку - это не так уж и плохо.
Зар
5

Есть несколько разных способов определить «стоимость». Есть фактические накладные расходы на получение и снятие блокировки; как пишет Джейк, это ничтожно мало, если эта операция не выполняется миллионы раз.

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

Keiths
источник