Ile kosztuje blokada?

111

Eksperymentowałem z wielowątkowością i przetwarzaniem równoległym i potrzebowałem licznika, aby wykonać podstawowe zliczanie i analizę statystyczną szybkości przetwarzania. Aby uniknąć problemów z jednoczesnym używaniem mojej klasy, użyłem instrukcji lock na zmiennej prywatnej w mojej klasie:

private object mutex = new object();

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

Ale zastanawiałem się ... ile kosztuje blokowanie zmiennej? Jakie są negatywne skutki dla wydajności?

Kees C. Bakker
źródło
10
Blokowanie zmiennej nie jest takie drogie; jest to oczekiwanie na zablokowaną zmienną, której chcesz uniknąć.
Gabe,
53
to dużo tańsze niż spędzanie godzin na śledzeniu innego stanu wyścigu ;-)
BrokenGlass
2
Cóż ... jeśli zamek jest drogi, możesz chcieć ich uniknąć, zmieniając programowanie tak, aby wymagało mniej blokad. Mógłbym zaimplementować jakąś synchronizację.
Kees C. Bakker
1
Miałem radykalną poprawę wydajności (teraz, po przeczytaniu komentarza @ Gabe), po prostu przenosząc dużo kodu z moich bloków blokujących. Konkluzja: od teraz zostawiam tylko zmienny dostęp (zwykle jeden wiersz) w bloku zamka, coś w rodzaju "blokowania w czasie". Czy jest sens?
heltonbiker
2
@heltonbiker Oczywiście, że ma to sens. Powinna to być również zasada architektoniczna, zamki mają być możliwie krótkie, proste i szybkie. Tylko naprawdę niezbędne dane, które trzeba zsynchronizować. Na serwerach należy również wziąć pod uwagę hybrydowy charakter zamka. Rywalizacja, nawet jeśli nie jest krytyczna dla twojego kodu, jest spowodowana hybrydowym charakterem zamka, który powoduje obracanie się rdzeni podczas każdego dostępu, jeśli blokada jest trzymana przez kogoś innego. Skutecznie pochłaniasz część zasobów procesora z innych usług na serwerze przez pewien czas, zanim wątek zostanie zawieszony.
ipavlu

Odpowiedzi:

86

Oto artykuł, który wchodzi w koszty. Krótka odpowiedź to 50ns.

Jake Pearson
źródło
39
Krótka lepsza odpowiedź: 50ns + czas spędzony na czekaniu, jeśli inny wątek trzyma blokadę.
Herman,
4
Im więcej wątków wchodzi i wychodzi z blokady, tym droższe. Koszt
rośnie
16
Pewien kontekst: podzielenie dwóch liczb na 3Ghz x86 zajmuje około 10ns (nie licząc czasu potrzebnego na pobranie / zdekodowanie instrukcji) ; a ładowanie pojedynczej zmiennej z (niebuforowanej) pamięci do rejestru zajmuje około 40ns. Tak więc 50ns jest niesamowicie, oślepiająco szybkie - nie powinieneś martwić się o koszt użycia lock, tak samo jak nie martwisz się o koszt użycia zmiennej.
BlueRaja - Danny Pflughoeft
3
Również ten artykuł był stary, kiedy zadano to pytanie.
Otis
3
Naprawdę świetne dane, „prawie nic nie kosztuje”, nie wspominając o błędach. Nie bierzecie pod uwagę, że jest krótki i szybki tylko i TYLKO wtedy, gdy nie ma żadnej rywalizacji, jeden wątek. W TAKIM PRZYPADKU W OGÓLE NIE POTRZEBUJESZ ZAMKA. Druga sprawa, blokada to nie blokada, ale blokada hybrydowa, wykrywa wewnątrz CLR, że zamek nie jest utrzymywany przez nikogo na podstawie operacji atomowych iw takim przypadku unika wywołań do rdzenia systemu operacyjnego, czyli innego pierścienia, który nie jest przez nie mierzony testy. Co jest mierzone jako 25ns do 50ns jest rzeczywiście poziom aplikacji zablokowane kod instrukcji, jeśli blokada nie zostanie podjęta
ipavlu
50

Odpowiedź techniczna jest taka, że ​​nie można tego określić ilościowo, w dużej mierze zależy to od stanu buforów zapisujących w pamięci procesora oraz od tego, ile danych zebranych przez moduł wstępnego pobierania danych musi zostać odrzuconych i ponownie odczytanych. Które są bardzo niedeterministyczne. Używam 150 cykli procesora jako przybliżenia, które pozwala uniknąć większych rozczarowań.

Odpowiedź praktyczne jest to, że jest waaaay tańsze niż ilość czasu spalisz na debugowanie kodu, gdy myślisz, że możesz pominąć blokadę.

Aby uzyskać twardą liczbę, musisz zmierzyć. Program Visual Studio ma zręczny analizator współbieżności dostępny jako rozszerzenie.

Hans Passant
źródło
1
Właściwie nie, można to określić ilościowo i zmierzyć. Po prostu nie jest to tak łatwe, jak napisanie tych zamków dookoła kodu, a następnie stwierdzenie, że to tylko 50 ns, mit mierzony na podstawie jednowątkowego dostępu do zamka.
ipavlu
8
„że można pominąć blokadę” ... Myślę, że to, gdzie wiele osób jest w gdy czytają to pytanie ...
Snoop
30

Dalsze czytanie:

Chciałbym przedstawić kilka moich artykułów, które są zainteresowane ogólnymi prymitywami synchronizacji i zagłębiają się w Monitor, zachowanie instrukcji blokady C #, właściwości i koszty w zależności od różnych scenariuszy i liczby wątków. W szczególności interesuje się marnotrawstwem procesora i okresami przepustowości, aby zrozumieć, ile pracy można wykonać w wielu scenariuszach:

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

Oryginalna odpowiedź:

O jej!

Wygląda na to, że poprawna odpowiedź oznaczona tutaj jako ODPOWIEDŹ jest z natury niepoprawna! Chciałbym prosić autora odpowiedzi, z szacunkiem, aby przeczytał do końca artykuł, do którego prowadzi link. artykuł

Autor artykułu z 2003 roku dokonywał pomiarów tylko na maszynie Dual Core iw pierwszym przypadku pomiarowym mierzył blokowanie tylko jednym gwintem i wynik wynosił około 50ns na dostęp do zamka.

Nie mówi nic o blokadzie w środowisku współbieżnym. Musimy więc kontynuować czytanie artykułu, aw drugiej połowie autor mierzył scenariusz blokowania z dwoma i trzema wątkami, który zbliża się do poziomów współbieżności dzisiejszych procesorów.

Tak więc autor mówi, że przy dwóch wątkach na Dual Core zamki kosztują 120ns, a przy 3 wątkach idzie to 180ns. Wydaje się więc, że jest to wyraźnie zależne od liczby wątków jednocześnie uzyskujących dostęp do blokady.

Jest to więc proste, nie jest to 50 ns, chyba że jest to pojedynczy wątek, w którym blokada staje się bezużyteczna.

Inną kwestią do rozważenia jest to, że jest mierzony jako średni czas !

Gdyby mierzyć czas iteracji, byłyby nawet czasy od 1 ms do 20 ms, po prostu dlatego, że większość była szybka, ale kilka wątków będzie czekało na czas procesora i będzie miało nawet milisekundowe opóźnienia.

To zła wiadomość dla każdego rodzaju aplikacji, która wymaga dużej przepustowości i małych opóźnień.

Ostatnią kwestią do rozważenia jest to, że wewnątrz zamka mogą występować wolniejsze operacje i bardzo często tak jest. Im dłużej blok kodu jest wykonywany wewnątrz zamka, tym większa rywalizacja i opóźnienia rosną niebotycznie.

Proszę wziąć pod uwagę, że od 2003 roku minęła już ponad dekada, czyli kilka generacji procesorów zaprojektowanych specjalnie do pracy w pełni równoległej, a blokowanie znacznie szkodzi ich wydajności.

ipavlu
źródło
1
Aby wyjaśnić, artykuł nie mówi, że wydajność blokady pogarsza się wraz z liczbą wątków w aplikacji; wydajność spada wraz z liczbą wątków rywalizujących o blokadę. (To jest dorozumiane, ale nie jest to jasno określone w powyższej odpowiedzi.)
Agrest
Zakładam, że masz na myśli to: „Wydaje się więc, że jest to wyraźnie zależne od liczby współbieżnie dostępnych wątków, a im więcej, tym gorzej”. Tak, sformułowanie mogłoby być lepsze. Miałem na myśli „równoczesny dostęp” jako wątki współbieżnie uzyskujące dostęp do blokady, tworząc w ten sposób rywalizację.
ipavlu
20

To nie odpowiada na twoje pytanie dotyczące wydajności, ale mogę powiedzieć, że .NET Framework oferuje Interlocked.Addmetodę, która pozwoli ci dodać amounttwój doneelement do swojego członka bez ręcznego blokowania innego obiektu.

Adam Maras
źródło
1
Tak, to chyba najlepsza odpowiedź. Ale głównie z powodu krótszego i czystszego kodu. Różnica prędkości raczej nie będzie zauważalna.
Henk Holterman
dzięki za tę odpowiedź. Robię więcej rzeczy z zamkami. Dodane int jest jednym z wielu. Uwielbiam tę sugestię, będę z niej korzystać od teraz.
Kees C. Bakker
zamki są o wiele łatwiejsze do uzyskania, nawet jeśli kod bez blokady jest potencjalnie szybszy. Zablokowane.Add ma takie same problemy jak + = bez synchronizacji.
hangar
10

lock (Monitor.Enter / Exit) jest bardzo tani, tańszy niż alternatywy, takie jak Waithandle lub Mutex.

Ale co by było, gdyby był (trochę) wolny, czy wolałbyś mieć szybki program z nieprawidłowymi wynikami?

Henk Holterman
źródło
5
Haha ... szedłem na szybki program i dobre wyniki.
Kees C. Bakker
@ henk-holterman Z Twoimi wypowiedziami wiąże się wiele problemów: Po pierwsze, jak jasno pokazało to pytanie i odpowiedzi, słabe zrozumienie wpływu blokady na ogólną wydajność, nawet ludzie twierdzą, że mit o 50 ns, który ma zastosowanie tylko w środowisku jednowątkowym. Po drugie, Twoje oświadczenie jest tutaj i pozostanie na lata iw międzyczasie, procesory rosną w rdzeniach, ale szybkość rdzeni nie jest tak duża. ** Aplikacje Thrid ** stają się tylko bardziej złożone w czasie, a potem jest warstwa po warstwie blokowanie w środowisku wielu rdzeni i ich liczba rośnie, 2,4,8,10,20,16,32
ipavlu
Moje zwykłe podejście polega na tworzeniu synchronizacji w luźny sposób przy jak najmniejszej interakcji. Działa to bardzo szybko w przypadku struktur danych bez blokad. Zrobiłem dla moich opakowań kodu wokół spinlock, aby uprościć programowanie, a nawet jeśli TPL ma specjalne kolekcje współbieżne, opracowałem własne kolekcje z blokadą spinową wokół listy, tablicy, słownika i kolejki, ponieważ potrzebowałem trochę więcej kontroli, a czasami kod działał pod spinlock. Mogę powiedzieć, że jest to możliwe i pozwala na rozwiązanie wielu scenariuszy, których nie są w stanie wykonać windykacje TPL i przy dużym wzroście wydajności / przepustowości.
ipavlu
7

Koszt zamka w ciasnej pętli w porównaniu z alternatywą bez zamka jest ogromny. Możesz sobie pozwolić na wielokrotne zapętlanie i nadal być bardziej wydajnym niż zamek. Dlatego kolejki bez blokad są tak wydajne.

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();
        }
    }
}

Wynik:

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
Johan Nilsson
źródło
4
To może być zły przykład, ponieważ twoja pętla naprawdę nic nie robi, poza przypisaniem jednej zmiennej i blokadą co najmniej 2 wywołania funkcji. Poza tym 20ns za blokadę nie jest takie złe.
Zar Shardan,
5

Istnieje kilka różnych sposobów definiowania „kosztu”. Istnieje rzeczywisty koszt uzyskania i zwolnienia blokady; Jak pisze Jake, jest to nieistotne, chyba że ta operacja zostanie wykonana miliony razy.

Bardziej istotny jest wpływ, jaki ma to na przebieg egzekucji. Ten kod można wprowadzić tylko w jednym wątku na raz. Jeśli masz 5 wątków wykonujących tę operację regularnie, 4 z nich będą czekać na zwolnienie blokady, a następnie będą pierwszym wątkiem zaplanowanym do wprowadzenia tego fragmentu kodu po zwolnieniu blokady. Więc twój algorytm znacznie ucierpi. To, jak bardzo zależy od algorytmu i jak często wywoływana jest operacja. Nie da się tego uniknąć bez wprowadzenia warunków wyścigu, ale można to poprawić, minimalizując liczbę wywołań zablokowanego kodu.

KeithS
źródło