Metody atomowe Bezpieczeństwo wątków i warunki wyścigu w C#

Metody atomowe Bezpieczeństwo wątków i warunki wyścigu w C#

Metody atomowe, bezpieczeństwo wątków i warunki wyścigu w C#

W tym artykule omówię Metody atomowe, bezpieczeństwo wątków i warunki wyścigowe w C# z przykładami. Przeczytaj nasz poprzedni artykuł, w którym omówiliśmy Jak anulować operacje równoległe w C# z przykładami.

Metody atomowe w C#:

Jak dotąd metody równoległe (For, Foreach i Invoke), które musimy wywołać, są całkowicie samowystarczalne. W tym sensie, że do działania nie potrzebują danych zewnętrznych. Ale nie zawsze tak będzie. Czasami będziemy chcieli udostępnić dane między wątkami. Ważną koncepcją, którą należy wziąć pod uwagę, jest koncepcja metod atomowych w C#. Metod atomowych można wygodnie używać w środowisku wielowątkowym, ponieważ gwarantują one determinizm, co oznacza, że ​​zawsze uzyskamy ten sam wynik, bez względu na to, ile wątków próbuje jednocześnie wykonać metodę.

Charakterystyka metod atomowych w C#:

Istnieją dwie podstawowe cechy metod atomowych w C#.

  1. Po pierwsze, jeśli jeden wątek wykonuje metodę atomową, inny wątek nie może zobaczyć stanu pośredniego, który oznacza, że ​​operacja nie została uruchomiona lub została już zakończona. Ale nie ma stanu pośredniego między początkiem a końcem.
  2. Po drugie, operacja zakończy się pomyślnie lub zakończy się niepowodzeniem bez wprowadzania jakichkolwiek modyfikacji. Ta część jest podobna do transakcji bazodanowych, gdzie albo wszystkie operacje kończą się powodzeniem, albo żadna nie jest wykonywana, jeśli wystąpi co najmniej jeden błąd.
Jak osiągnąć atomowość w C#?

Istnieje kilka sposobów na osiągnięcie atomowości w C#. Najczęstszym sposobem jest użycie zamków. Blokady pozwalają nam blokować inne wątki przed wykonaniem fragmentu kodu, gdy blokada jest aktywna. Jeśli pracujemy z kolekcjami, inną opcją jest użycie kolekcji współbieżnych, które są specjalnie zaprojektowane do obsługi scenariuszy wielowątkowych. Jeśli nie użyjemy odpowiednich mechanizmów zapewniających automatyzację naszych metod lub operacji, otrzymamy nieoczekiwane wyniki, uszkodzone dane lub nieprawidłowe wartości.

Bezpieczeństwo wątków w C#:

Ważną koncepcją w środowisku równoległości jest bezpieczeństwo wątków. Kiedy mówimy, że metoda jest bezpieczna wątkowo, mówimy, że możemy wykonać tę metodę jednocześnie z wielu wątków bez powodowania jakiegokolwiek błędu. Wiemy, że mamy bezpieczeństwo wątków, gdy dane aplikacji nie są uszkodzone, jeśli dwa lub więcej wątków próbuje jednocześnie wykonywać operacje na tych samych danych.

Jak osiągnąć bezpieczeństwo wątków w C#?

Co musimy zrobić, aby mieć metodę bezpieczną wątkowo w C#? Cóż, wszystko zależy od tego, co zrobimy w ramach metody. Jeśli w ramach metody dodaliśmy zmienną zewnętrzną. Wtedy moglibyśmy mieć problem z nieoczekiwanymi wynikami w tej zmiennej. Coś, czego możemy użyć, aby to złagodzić, to użycie mechanizmu synchronizacji, takiego jak użycie Interlocked lub użycie zamków.

Jeśli musimy przekształcić obiekty, możemy użyć niezmiennych obiektów, aby uniknąć problemów z uszkodzeniem tych obiektów.

Idealnie powinniśmy pracować z czystymi funkcjami. Czyste funkcje to te, które zwracają tę samą wartość dla tych samych argumentów i nie powodują efektów wtórnych.

Warunki wyścigu w C#:

Warunki wyścigu występują w C#, gdy mamy zmienną współdzieloną przez kilka wątków i te wątki chcą modyfikować zmienne jednocześnie. Problem z tym 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. Operacje są proste, jak zwiększenie o jeden.

Zmienna jest problematyczna, jeśli robimy je w scenariuszach wielowątkowych na wspólnej zmiennej. Powodem jest to, że nawet zwiększenie zmiennej o 1 lub dodanie 1 do zmiennej jest problematyczne. Dzieje się tak, ponieważ operacja nie jest atomowa. Prosta inkrementacja zmiennej nie jest operacją niepodzielną.

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.

Przykład zrozumienia warunków wyścigu w C#:

Na przykład w poniższej tabeli, co się stanie, jeśli dwa wątki po kolei spróbują zwiększyć wartość zmiennej. Mamy Wątek 1 w kolumnie pierwszej i Wątek 2 w kolumnie 2. I na końcu kolumna wartości reprezentuje wartość zmiennej. Aby lepiej zrozumieć, spójrz na poniższy diagram.

Początkowo wartość zmiennej wynosi zero. Wątek 1 ze zmienną, a następnie w pamięci ma wartość 0. Następnie Wątek 1 ponownie zwiększa tę wartość w pamięci, a na koniec dostarcza tę wartość do zmiennej. A następnie wartość zmiennej wynosi 1. Aby lepiej zrozumieć, spójrz na poniższy diagram.

Następnie po tym wątku 2 odczytuje wartość zmiennej, która ma teraz wartość 1, zwiększa wartość w pamięci. I na koniec zapisuje z powrotem do zmiennej. A wartość tej zmiennej wynosi teraz 2. Aby lepiej zrozumieć, spójrz na poniższy diagram.

Jest to zgodne z oczekiwaniami. Co jednak może się stać, jeśli dwa wątki spróbują zaktualizować zmienną jednocześnie?

Co się stanie, jeśli dwa wątki spróbują zaktualizować zmienną jednocześnie?

Cóż, wynik może być taki, że ostateczna wartość zmiennej to 1 lub 2. Powiedzmy, że jedna możliwość. Proszę spojrzeć na poniższy schemat. Tutaj znowu mamy Wątek 1, Wątek 2 i wartość zmiennej.

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.

Trzeci 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. Tak więc, nawet jeśli 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ć jedna. Czasami wartość zmiennej może wynosić dwa. Wszystko zależy od przypadku.

Jak rozwiązać powyższy problem w C#?

Możemy użyć mechanizmów synchronizacji. 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. Następnie zobaczymy, jak użyć blokady do rozwiązania problemu wyścigu.

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ą ValueWithoutInterlocked. Tutaj, ponieważ wykonujemy pętlę 100000 razy, oczekujemy, że wartość ValueWithoutInterlocked wyniesie 100000.

using System;
using System.Threading.Tasks;

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

Teraz uruchom powyższy kod wiele razy, a za każdym razem otrzymasz różne wyniki, 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 ParallelProgrammingDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var ValueInterlocked = 0;
            Parallel.For(0, 100000, _ =>
            {
                //Incrementing the value
               Interlocked.Increment(ref ValueInterlocked);
            });
            Console.WriteLine("Expected Result: 100000");
            Console.WriteLine($"Actual Result: {ValueInterlocked}");
            Console.ReadKey();
        }
    }
}
Wyjście:

Jak widać na powyższym obrazie wyjściowym, otrzymujemy rzeczywisty wynik jako wynik oczekiwany. 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, czyniąc operację przyrostową Atomic. 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, w celu wykonania operacji atomowych na zmiennej.

Czasami Splecione nie wystarczają. Czasami nie mamy wielu wątków, aby uzyskać dostęp do sekcji krytycznej. Chcemy, aby dostęp do sekcji krytycznej miał tylko jeden wątek. W tym celu możemy użyć zamka.

Zablokuj w C#:

Kolejnym mechanizmem, którego możemy użyć do edycji danych przez wiele wątków jednocześnie, jest blokada. z blokadą możemy mieć blok kodu, który będzie wykonywany tylko przez jeden wątek na raz. Oznacza to, że 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.

Należy wziąć pod uwagę, że w idealnym przypadku to, co robimy wewnątrz bloku blokady, powinno być stosunkowo szybkie. Dzieje się tak, ponieważ wątki są blokowane podczas oczekiwania na zwolnienie blokady. A jeśli przez dłuższy czas masz zablokowanych wiele wątków, może to mieć wpływ na szybkość Twojej aplikacji.

Przykład zrozumienia blokady w C#:

Przepiszmy poprzedni przykład za pomocą kłódki. Proszę spojrzeć na poniższy przykład. Zaleca się posiadanie dedykowanego obiektu do zamka. Chodzi o to, że tworzymy zamki oparte na obiektach.

using System;
using System.Threading.Tasks;

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

        static void Main(string[] args)
        {
            var ValueWithLock = 0;
            Parallel.For(0, 100000, _ =>
            {
                lock(lockObject)
                {
                    //Incrementing the value
                    ValueWithLock++;
                }
            });
            Console.WriteLine("Expected Result: 100000");
            Console.WriteLine($"Actual Result: {ValueWithLock}");
            Console.ReadKey();
        }
    }
}
Wyjście:

W następnym artykule omówię Interlock vs Lock w C# z przykładami. Tutaj, w tym artykule, staram się Metody atomowe, bezpieczeństwo wątków i warunki wyścigu w C# z przykładami. Mam nadzieję, że spodoba ci się ta metoda atomowa, bezpieczeństwo wątków i warunki wyścigu w C# z przykładami.