Наскільки дорого коштує заява про блокування?


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 секунд + витрачений час на очікування, якщо інша нитка тримає замок.
Герман

4
Чим більше ниток входить і залишає замок, тим дорожче він стає. Вартість зростає експоненціально з кількістю ниток
Арсен Захрей

16
Деякий контекст: ділення двох чисел на 3Ghz x86 займає близько 10н (не враховуючи часу, необхідного для отримання / декодування інструкції) ; і завантаження однієї змінної з (не кешованої) пам'яті в регістр займає близько 40н. Тож 50ns божевільно, сліпо швидко - вам не варто турбуватися про витрати на використання lockбільше, ніж ви турбуєтесь про вартість використання змінної.
BlueRaja - Danny Pflughoeft

3
Крім того, ця стаття була давньою, коли це питання було задано.
Отіс

3
Дійсно чудова метрика, "майже без витрат", не кажучи вже про неправильну. Ви, хлопці, не враховуйте, що це лише коротко і швидко і ТІЛЬКИ, якщо взагалі немає суперечок, одна нитка. У такому випадку вам НЕ ПОТРІБНО ЗАКЛЮЧАТИ ВСІ. По-друге, замок - це не блокування, а гібридний замок, він виявляє всередині CLR, що блокування не проводиться ніким на основі атомних операцій, і в такому випадку він уникає дзвінків до ядра операційної системи, тобто іншого кільця, яке не вимірюється цими тести. Що вимірюється як 25ns до 50ns, це насправді код застосованого
блоку

50

Технічна відповідь полягає в тому, що це неможливо кількісно оцінити, це сильно залежить від стану буферів для запису оперативної пам’яті процесора та того, скільки даних, які зібрав префетер, потрібно викинути та перечитати. Які обидва є недетермінованими. Я використовую 150 циклів процесора як наближення до конверту, що дозволяє уникнути великих розчарувань.

Практична відповідь полягає в тому, що це набагато дешевше, ніж кількість часу, яке ви будете спалювати, налагоджуючи код, коли думаєте, що можете пропустити замок.

Щоб отримати важке число, вам доведеться виміряти. Visual Studio має гладкий аналізатор паралельності, доступний як розширення.


1
Насправді ні, її можна кількісно оцінити і виміряти. Це просто не так просто, як писати ці замки по всьому коду, а потім заявляти, що це всього лише 50ns, міф, виміряний на одному потоковому доступі до замка.
ipavlu

8
"Думаю, ти можеш пропустити замок" ... Я думаю, що там багато людей, коли вони читають це питання ...
Snoop

30

Подальше читання:

Я хотів би представити кілька моїх статей, які цікавляться загальними примітивами синхронізації, і вони копаються у поведінці, властивостях та витратах оператора замкнення 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, і в першому випадку вимірювання він вимірював блокування лише однією ниткою, і результат становив близько 50н за доступ до блокування.

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

Так автор каже, що з двома потоками на Dual Core, блоки коштують 120ns, а з 3-х потоків - це 180ns. Таким чином, здається, це чітко залежить від кількості потоків, що одночасно отримують доступ до блокування.

Так що це просто, це не 50 нс, якщо це не єдина нитка, де замок стає марним.

Іншим питанням для розгляду є те, що він вимірюється як середній час !

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

Це погана новина для будь-яких програм, які вимагають високої пропускної здатності та низької затримки.

І останнє питання для розгляду полягає в тому, що всередині замка можуть бути повільніші операції, і дуже часто це відбувається. Чим довше блок коду виконується всередині блокування, тим вища суперечка і затримки піднімаються на висоту.

Зауважте, що вже минуло десятиліття вже з 2003 року, що мало поколінь процесорів, призначених спеціально для одночасного запуску, і блокування значно погіршує їх продуктивність.


1
Для уточнення, стаття не говорить про те, що продуктивність блокування погіршує кількість потоків у програмі; продуктивність знижується з кількістю потоків, що змагаються над замком. (Це мається на увазі, але чітко не зазначено у відповіді вище.)
Агрус

Я припускаю, що ви це маєте на увазі: "Отже, це, мабуть, чітко залежить від кількості одночасно доступних потоків, і більше гірше" Так, формулювання може бути кращим. Я мав на увазі "паралельний доступ", як нитки одночасно отримують доступ до блокування, створюючи таким чином суперечку.
ipavlu

20

Це не відповідає вашому запиту щодо продуктивності, але можу сказати, що .NET Framework пропонує Interlocked.Addметод, який дозволить вам додати amountсвій doneчлен до свого учасника, не замикаючись вручну на іншому об'єкті.


1
Так, це, мабуть, найкраща відповідь. Але в основному з причини коротшого та більш чистого коду. Різниця в швидкості, ймовірно, не помітна.
Хенк Холтерман

дякую за цю відповідь Я роблю більше речей із замками. Додані вставки - одна з багатьох. Любіть пропозицію, використовуйте її відтепер.
Kees C. Bakker

замки набагато, набагато простіше виправитись, навіть якщо код без блокування потенційно швидше. Interlocked.Add самостійно має ті самі проблеми, що і + = без синхронізації.
ангар

10

lock (Monitor.Enter / Exit) - це дуже дешево, дешевше, ніж такі альтернативи, як Waithandle або Mutex.

Але що робити, якщо це було (трохи) повільно, ви б швидше мали швидку програму з неправильними результатами?


5
Ха-ха ... Я їхав за швидкою програмою та хорошими результатами.
Kees C. Bakker

@ henk-holterman З вашими твердженнями виникає декілька питань: По-перше, як це питання і відповіді чітко показали, недостатньо розуміється вплив блокування на загальну продуктивність, навіть люди, які заявляють про міф про 50ns, який застосовний лише для однопотокового середовища. По-друге, ваше твердження є тут і залишатиметься роками, а середній час - процесори, вирощені в ядрах, але швидкість ядер не настільки велика. ** Тридцять ** додатки з часом стають лише складнішими, і тоді це шар за шаром замикання в середовищі багатьох ядер, і кількість збільшується, 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 виклики функції. Крім того, 20ns за замок, який ви отримуєте, не так вже й погано.
Зар Шардан

5

Існує кілька різних способів визначення "вартості". Є фактичні витрати на отримання та звільнення замка; як пише Джейк, це незначно, якщо ця операція не проводиться мільйони разів.

Більш актуальним є вплив, який це має на потік виконання. Цей код можна вводити одночасно лише одним потоком. Якщо у вас є 5 потоків, що виконують цю операцію на регулярній основі, 4 з них в кінцевому підсумку очікують звільнення блокування, а потім буде першим потоком, який планується ввести цей фрагмент коду після звільнення цього блокування. Отже, ваш алгоритм значно постраждає. Скільки залежить від алгоритму та того, як часто викликається операція .. Ви не можете реально уникнути цього, не запровадивши гоночні умови, але ви можете покращити його, зменшивши кількість викликів до заблокованого коду.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.