Interlocked vs Lock w C#

Interlocked vs Lock w C#

Zablokowane vs Lock w C# z przykładami:

W tym artykule omówię Interlocked vs Lock w C# z przykładami. Przeczytaj nasz poprzedni artykuł, w którym omówiliśmy Metody atomowe, bezpieczeństwo wątków i warunki wyścigu w C# z przykładami. W tym artykule najpierw omówimy Interlocked, a następnie omówimy Lock. Następnie zobaczymy test porównawczy wydajności między Interlocked a Lock w C#, a na koniec omówimy, kiedy używać Lock over Interlocked i na odwrót.

Przykład zrozumienia funkcji Interlocked w C#:

W C# wyścigi występują, gdy mamy zmienną współdzieloną przez kilka wątków i te wątki chcą modyfikować zmienną jednocześnie. Problem polega na tym, że w zależności od kolejności sekwencji operacji wykonywanych na zmiennej przez różne wątki, wartość zmiennej będzie inna.

Zmienna jest problematyczna, jeśli uzyskujemy do niej dostęp w środowisku wielowątkowym. Nawet zwiększenie zmiennej o 1 lub dodanie zmiennych o 1 jest problematyczne. Dzieje się tak, ponieważ operacja nie jest atomowa. Prosta inkrementacja zmiennej nie jest operacją atomową.

W rzeczywistości jest podzielony na trzy części:czytanie, zwiększanie i pisanie. Biorąc pod uwagę fakt, że mamy trzy operacje, dwa wątki mogą je wykonać w taki sposób, że nawet jeśli dwukrotnie zwiększymy wartość zmiennej, zadziała tylko jeden wzrost.

Co się stanie, jeśli dwa wątki po kolei spróbują zwiększyć zmienną. Zrozummy to na przykładzie. Proszę spojrzeć na poniższą tabelę. Tutaj mamy Wątek 1 w kolumnie pierwszej i Wątek 2 w kolumnie 2. Na końcu kolumna wartości reprezentuje wartość zmiennej. W tym przypadku wynik może być taki, że ostateczna wartość zmiennej to 1 lub 2. Zobaczmy jedną możliwość.

Teraz, Wątek 1 i Wątek 2 czytają wartości, więc oba mają wartość zero w pamięci. Aby lepiej zrozumieć, spójrz na poniższy obraz.

Wątek 1 zwiększa wartość, podobnie jak Wątek 2, również zwiększa wartość i oba zwiększają ją do 1 w pamięci. Aby lepiej zrozumieć, spójrz na poniższy obraz.

Gdy oba wątki zwiększają wartość do 1 w pamięci. Następnie Wątek 1 zapisuje z powrotem do zmiennej 1, a Wątek 2 również zapisuje z powrotem do zmiennej 1, jeszcze raz. Aby lepiej zrozumieć, spójrz na poniższy obraz.

Oznacza to, że jak widać, w zależności od kolejności wykonania metod będziemy ustalać wartość zmiennej. Mimo że zwiększyliśmy wartość dwukrotnie w różnych wątkach, ponieważ byliśmy w środowisku wielowątkowym, mieliśmy warunek Race, co oznacza, że ​​teraz nie mamy operacji deterministycznej, ponieważ czasami może to być jeden, a czasami dwa.

Jak rozwiązać powyższy problem?

Istnieje wiele sposobów rozwiązania powyższego problemu. Pierwszym mechanizmem, któremu przyjrzymy się, aby poradzić sobie z problemami związanymi z edytowaniem zmiennej przez wiele wątków, jest Interlocked.

Powiązane w C#:

Klasa Interlocked w C# pozwala nam wykonywać pewne operacje w sposób niepodzielny, co czyni tę operację bezpieczną do wykonania z różnych wątków na tej samej zmiennej. Oznacza to, że klasa Interlocked daje nam kilka metod, które pozwalają nam wykonywać pewne operacje bezpiecznie lub niepodzielnie, nawet jeśli kod będzie wykonywany przez kilka wątków jednocześnie.

Przykład zrozumienia funkcji Interlocked w C#:

Najpierw zobaczymy przykład bez użycia Interlocked i zobaczymy problem, a następnie przepiszemy ten sam przykład używając Interlocked i zobaczymy, jak interlocked rozwiązuje problem bezpieczeństwa wątków.

Proszę spojrzeć na poniższy przykład. W poniższym przykładzie zadeklarowaliśmy zmienną i za pomocą pętli Parallel For zwiększamy jej wartość. Jak wiemy, pętla Parallel For wykorzystuje wielowątkowość, więc wiele wątków próbuje zaktualizować (zwiększyć) tę samą zmienną IncrementValue. Tutaj, ponieważ wykonujemy pętlę 100000 razy, oczekujemy, że wartość IncrementValue będzie wynosić 100000.

using System;
using System.Threading.Tasks;

namespace InterlockedDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var IncrementValue = 0;
            Parallel.For(0, 100000, _ =>
            {
                //Incrementing the value
                IncrementValue++;
            });
            Console.WriteLine("Expected Result: 100000");
            Console.WriteLine($"Actual Result: {IncrementValue}");
            Console.ReadKey();
        }
    }
}

Teraz uruchom powyższy kod wiele razy, a za każdym razem otrzymasz inny wynik, a także zobaczysz różnicę między rzeczywistym wynikiem a oczekiwanym wynikiem, jak pokazano poniżej obraz.

Przykład użycia klasy Interlocked w C#:

Klasa Interlocked w C# udostępnia jedną statyczną metodę o nazwie Increment. Metoda Increment zwiększa określoną zmienną i przechowuje wynik jako operację niepodzielną. Tak więc tutaj musimy określić zmienną za pomocą słowa kluczowego ref, jak pokazano w poniższym przykładzie.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace InterlockedDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var IncrementValue = 0;
            Parallel.For(0, 100000, _ =>
            {
                //Incrementing the value
                Interlocked.Increment(ref IncrementValue);
            });
            Console.WriteLine("Expected Result: 100000");
            Console.WriteLine($"Actual Result: {IncrementValue}");
            Console.ReadKey();
        }
    }
}
Wyjście:

Teraz, bez względu na to, ile razy wykonasz powyższy kod, otrzymasz ten sam wynik. Jak widać na powyższym obrazie wyjściowym, otrzymujemy rzeczywisty wynik jako oczekiwany wynik. Tak więc Klasa Interlocked zapewnia niepodzielne operacje dla zmiennych, które są współużytkowane przez wiele wątków. Oznacza to, że mechanizm synchronizacji Interlocked pozwala nam uniknąć sytuacji wyścigu poprzez wykonanie operacji inkrementacji Atomic.

Co to jest Interlocked Class w C#?

Jeśli przejdziesz do definicji klasy Interlocked, zobaczysz, że ta klasa zapewnia wiele metod statycznych, takich jak Increment, Decrement, Add, Exchange itp., jak pokazano na poniższym obrazku do wykonywania operacji atomowych na zmiennej. Klasa Interlocked należy do przestrzeni nazw System.Threading.

Poniższe są metody dostarczane przez klasę C# Interlocked.

  1. Przyrost(): Ta metoda służy do zwiększania wartości zmiennej i przechowywania jej wyniku. Jego dopuszczalne parametry to liczby całkowite Int32 i Int64.
  2. Dekrementacja(): Ta metoda służy do dekrementacji wartości zmiennej i przechowywania jej wyniku. Jego dopuszczalne parametry to liczby całkowite Int32 i Int64.
  3. Exchange(): Ta metoda służy do wymiany wartości między zmiennymi. Ta metoda ma siedem przeciążonych wersji opartych na różnych typach, które może zaakceptować jako swój parametr.
  4. PorównajExchange(): Ta metoda porównuje dwie zmienne i przechowuje wynik porównania w innej zmiennej. Ta metoda ma również siedem przeciążonych wersji.
  5. Dodaj(): Ta metoda służy do dodawania dwóch zmiennych całkowitych i aktualizowania wyniku w pierwszej zmiennej całkowitej. Służy do dodawania liczb całkowitych typu Int32 oraz Int64.
  6. Odczyt(): Ta metoda służy do odczytywania zmiennej całkowitej. Służy do odczytywania liczby całkowitej typu Int64.

Więc zamiast operatorów dodawania, odejmowania i przypisania możemy użyć metod Add, Increment, Decrement, Exchange i CompareExchange. Widzieliśmy już przykład metody Increment. Zobaczmy teraz przykłady innych metod statycznych klasy Interlocked w C#.

Interlocked.Add Method w C#:

W klasie Interlocked dostępne są dwie przeciążone wersje metody Add. Są one następujące:

  1. publiczny statyczny długi Add(ref długa lokalizacja1, długa wartość): Ta metoda dodaje dwie 64-bitowe liczby całkowite i zastępuje pierwszą liczbę całkowitą sumą, jako operacja niepodzielna.
  2. publiczny statyczny int Add(ref int location1, int value): Ta metoda dodaje dwie 32-bitowe liczby całkowite i zastępuje pierwszą liczbę całkowitą sumą jako operację niepodzielną. Zwraca nową wartość przechowywaną w lokalizacji1.

Oto parametry:

  1. lokalizacja1: Zmienna zawierająca pierwszą wartość do dodania. Suma dwóch wartości jest przechowywana w lokalizacji 1.
  2. wartość: Wartość do dodania do zmiennej location1.
Przykład zrozumienia zablokowanej metody dodawania w C#:

Poniższy przykład pokazuje użycie metody Add klasy Interlocked.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace InterlockedDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            long SumValueWithoutInterlocked = 0;
            long SumValueWithInterlocked = 0;
            Parallel.For(0, 100000, number =>
            {
                SumValueWithoutInterlocked = SumValueWithoutInterlocked + number;
                Interlocked.Add(ref SumValueWithInterlocked, number);
            });
            
            Console.WriteLine($"Sum Value Without Interlocked: {SumValueWithoutInterlocked}");
            Console.WriteLine($"Sum Value With Interlocked: {SumValueWithInterlocked}");
            
            Console.ReadKey();
        }
    }
}
Wyjście:

Jak widać na powyższym obrazku, wartość sumy z blokadą zawsze daje ten sam wynik, podczas gdy wartość sumy bez blokady daje inny wynik. Oznacza to, że metoda Interlocked.Add zapewnia bezpieczeństwo wątków współdzielonej zmiennej.

Metoda wymiany i CompareExchange klasy zablokowanej:

Metoda Exchange klasy Interlocked w C# polega na niepodzielnej wymianie wartości określonych zmiennych. Druga wartość może być wartością zakodowaną na stałe lub zmienną. Tylko pierwsza zmienna w pierwszym parametrze zostanie zastąpiona przez drugą. Aby lepiej zrozumieć, spójrz na poniższy obraz.

Metoda CompareExchange klasy Interlocked w C# służy do łączenia dwóch operacji. Porównywanie dwóch wartości i przechowywanie trzeciej wartości w jednej ze zmiennych na podstawie wyniku porównania. Jeśli oba są równe, zastąp ten użyty jako pierwszy parametr podaną wartością. Aby lepiej zrozumieć, spójrz na poniższy obraz. Tutaj tworzymy zmienną całkowitą, a następnie przypisujemy jej wartość 20. Następnie wywołujemy metodę Interlocked.CompareExchange, aby porównać zmienną x z 20, a ponieważ obie są takie same, zastąpi x z DateTime. Ale już. Dzień, bieżący dzień miesiąca.

Przykład zrozumienia metody Interlocked Exchange i CompareExchange w C#
using System;
using System.Threading;
namespace InterlockedDemo
{
    class Program
    {
        static long x;
        static void Main(string[] args)
        {
            Thread thread1 = new Thread(new ThreadStart(SomeMethod));
            thread1.Start();
            thread1.Join();

            // Written [20]
            Console.WriteLine(Interlocked.Read(ref Program.x));

            Console.ReadKey();
        }

        static void SomeMethod()
        {
            // Replace x with 20.
            Interlocked.Exchange(ref Program.x, 20);

            // CompareExchange: if x is 20, then change to current DateTime.Now.Day or any integer variable.
            //long result = Interlocked.CompareExchange(ref Program.x, DateTime.Now.Day, 20);
            long result = Interlocked.CompareExchange(ref Program.x, 50, 20);

            // Returns original value from CompareExchange
            Console.WriteLine(result);
        }
    }
}

Wyjście:
20
50

Zablokowane a zablokowane w C# z punktu widzenia wydajności:

Bardzo łatwo jest używać metod Interlocked w programach. Ale czy naprawdę działa szybciej niż blokada? Zobaczmy to na przykładzie. W tym benchmarku pokazaliśmy 2 podejścia w C#.

  1. Wersja 1:Testujemy blokadę przed przyrostem liczby całkowitej w pierwszej pętli. Ten kod jest dłuższy i nie używa funkcji Interlocked.
  2. Wersja 2:To jest druga wersja kodu. Testujemy wywołanie Interlocked.Increment w drugiej pętli.
using System;
using System.Diagnostics;
using System.Threading;
namespace InterlockedDemo
{
    class Program
    {
        static object lockObject = new object();
        static int _test = 0;
        const int _max = 10000000;
        static void Main()
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            // Version 1: use lock.
            for (int i = 0; i < _max; i++)
            {
                lock (lockObject)
                {
                    _test++;
                }
            }
            stopwatch.Stop();
            Console.WriteLine($"Result using Lock: {_test}");
            Console.WriteLine($"Lock took {stopwatch.ElapsedMilliseconds} Milliseconds");

            //Reset the _test value
            _test = 0;
            stopwatch.Restart();
            
            // Version 2: use Interlocked.
            for (int i = 0; i < _max; i++)
            {
                Interlocked.Increment(ref _test);
            }
            stopwatch.Stop();
            Console.WriteLine($"Result using Interlocked: {_test}");
            Console.WriteLine($"Interlocked took {stopwatch.ElapsedMilliseconds} Milliseconds");
            Console.ReadKey();
        }
    }
}
Wyjście:

Tutaj możesz zobaczyć, że wynik jest poprawny w obu podejściach, ponieważ wydrukowana wartość jest równa całkowitej liczbie operacji przyrostowych. Jeśli zauważysz, że Interlocked.Increment był kilka razy szybszy, wymagając tylko 103 milisekund w porównaniu z 290 milisekund dla konstrukcji blokady. Czas może się różnić w zależności od komputera.

Kiedy używać funkcji Lock over Interlocked w C#?

Tak więc, jeśli to samo zadanie zostanie osiągnięte przy użyciu zarówno blokady, jak i blokady wątkowej, zaleca się użycie Interlocked w C#. Jednak w niektórych sytuacjach jest sytuacja, w której Interlocked nie zadziała i w takich sytuacjach musimy użyć blokady. Zrozummy to na przykładzie. Proszę spojrzeć na poniższy kod.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace InterlockedDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            long IncrementValue= 0;
            long SumValue = 0;
            Parallel.For(0, 100000, number =>
            {
                Interlocked.Increment(ref IncrementValue);
                Interlocked.Add(ref SumValue, IncrementValue);
            });
            
            Console.WriteLine($"Increment Value With Interlocked: {IncrementValue}");
            Console.WriteLine($"Sum Value With Interlocked: {SumValue}");

            Console.ReadKey();
        }
    }
}
Wyjście:

Jak widać na powyższym wyjściu, otrzymujemy inną wartość sumy nawet po użyciu opcji Interlocked. Czemu? Dzieje się tak, ponieważ istnieje warunek rasy. Wtedy możesz pomyśleć, że używamy metody Interlocked.Add i nie powinno być żadnych warunków wyścigu. Prawidłowy? Ale istnieje warunek wyścigu z następujących powodów.

Indywidualne metody Increment i Add są bezpieczne dla wątków, ale połączenie tych dwóch metod nie jest bezpieczne dla wątków. Aby lepiej zrozumieć, pomyśl o kodzie w następujący sposób. Jeden wątek rozpoczyna wykonywanie metody Increment. Podczas gdy wątek podróżuje do metody Add, inny wątek może uzyskać szansę na wykonanie metody Increment, która ponownie zmieni wartość IncrementValue. I dlatego wartość zmiennej IncrementValue została już zwiększona, zanim pierwsze zagrożenie zdążyło dokonać tej sumy. To jest powód, dla którego istnieje stan ryzyka.

Tak więc pomiędzy tymi dwiema operacjami, tj. Inkrementacją i Dodaj, występuje warunek wyścigu. Indywidualnie oba są bezpieczne dla wątków, razem nie są bezpieczne dla wątków, ponieważ podczas gdy wątek jeden przemieszcza się z metody Increment do metody Add Method, wiele, wiele, wiele wątków może wykonać metodę Increment. I dlatego istnieje wyścig.

Jak rozwiązać powyższy warunek wyścigu w C#?

Ponieważ mamy kilka operacji i chcemy, aby były wykonywane tylko przez jeden wątek na raz, możemy użyć blokady. Aby użyć blokady, musimy utworzyć instancję obiektu. Zaleca się posiadanie dedykowanego obiektu do zamka. Pomysł polega na tym, że wykonujemy zamki oparte na przedmiotach. Dla lepszego zrozumienia spójrz na poniższy przykład. Jakikolwiek kod jest obecny przed i po bloku blokady, zostanie wykonany równolegle, a kod bloku blokady będzie wykonywany sekwencyjnie, tj. tylko jeden wątek może uzyskać dostęp do bloku blokady na raz.

Tak więc, jeśli są, powiedzmy, dwa wątki próbujące uzyskać dostęp do bloku blokady, tylko jeden wątek będzie mógł wejść podczas oczekiwania zamówienia. A kiedy wątek pierwszy wyjdzie z bloku blokady, wątek drugi będzie mógł wejść do bloku blokady i uruchomić dwa wiersze kodu. Poniżej znajduje się kompletny przykładowy kod.

using System;
using System.Threading.Tasks;

namespace InterlockedDemo
{
    class Program
    {
        static object lockObject = new object();

        static void Main(string[] args)
        {
            long IncrementValue= 0;
            long SumValue = 0;
            
            Parallel.For(0, 10000, number =>
            {
                //Before lock Parallel 

                lock(lockObject)
                {
                    IncrementValue++;
                    SumValue += IncrementValue;
                }

                //After lock Parallel 
            });
            
            Console.WriteLine($"Increment Value With lock: {IncrementValue}");
            Console.WriteLine($"Sum Value With lock: {SumValue}");

            Console.ReadKey();
        }
    }
}
Wyjście:

Za każdym razem, gdy uruchamiamy aplikację, otrzymujemy ten sam wynik i otrzymujemy ten sam wynik, ponieważ używamy mechanizmu synchronizacji, który pozwala nam zabezpieczyć wiele wątków operacji.

Ograniczamy część naszego kodu do sekwencyjnego, nawet jeśli kilka wątków próbuje wykonać ten kod w tym samym czasie. Używamy blokad, gdy musimy wykonać kilka operacji lub operację nieobjętą Interlocked.

Uwaga: Zachowaj ostrożność podczas korzystania z zamka. Zawsze miej dedykowany obiekt dla blokady w C#. Nie próbuj ponownie używać obiektów, a także staraj się zachować prostotę. Postaraj się wykonać jak najmniej pracy wewnątrz blokady, ponieważ zbyt duża ilość pracy wewnątrz blokady może mieć wpływ na wydajność aplikacji.

W następnym artykule omówię Parallel LINQ lub PLINQ w C# z przykładami. Tutaj, w tym artykule, próbuję Interlocked vs Lock w C# z przykładami. Mam nadzieję, że spodoba ci się ten Interlocked vs Lock w C# z przykładami.