Wielowątkowość w C# - Thread, Lock, SpinLock
#1
Często zdarza się, że mamy potrzebę wykonania kilku rzeczy w tym samym czasie. Przykładowo, pijąc herbatę możemy równocześnie oglądać telewizję czy czytać książkę. Podobnie w programowaniu – podczas (przykładowo) wykonywania jakiejś dłuższej pętli nie chcemy zawieszać programu (na czas jej wykonania), tylko wykonać w tym samym czasie inne operacje.
W tym poście omówię pewną klasę i strukturę. Pierwsza z nich to Thread zawierający się w System.Threading; Pozwala on na wykonanie innej funkcji równolegle, nie naruszając/pauzując działania kodu. Przykładowo:

Kod:
using System;
using System.Threading;

namespace test
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread simpleThread1 = new Thread(new ThreadStart(doLoop));
            Thread simpleThread2 = new Thread(new ThreadStart(doLoop2));
            simpleThread1.Start();
            simpleThread2.Start();
            Console.ReadLine();
        }

        static void doLoop()
        {
            for (int i = 0; i != 10; i++)
            {
                Console.Write(i + " I am from thread doLoop!\n");
                Thread.Sleep(100);
            }
        }
        static void doLoop2()
        {
            for (int i = 0; i != 10; i++)
            {
                Console.Write(i + " I am from thread doLoop2!\n");
                Thread.Sleep(100);
            }
        }
    }
}

Daje wyjście:
[Obrazek: bezc2a0tytuc582u.png?w=842]
Jak widać dwa wątki (doLoop() oraz doLoop2()) wykonywane są w tym samym czasie, równolegle w stosunku do siebie.

Synchronizacja

Wyobraźmy sobie pewną sytuację. Próbujemy stworzyć pewien plik, nazwijmy go plik.txt i niech zostanie w przykładowym katalogu C:\Temp\. Najpierw musimy jednak sprawdzić, czy on istnieje. Przykładowo:
Kod:
static void Main(string[] args)
{
   if(!System.IO.File.Exists("C:\\Temp\\plik.txt") System.IO.File.Create("C:\\Temp\\plik.txt");
   else
   {
       System.IO.File.Delete("C:\\Temp\\plik.txt");
       System.IO.File.Create("C:\\Temp\\plik.txt");
   }
}
Kod ten sprawdza czy plik plik.txt istnieje, jeżeli tak usuwa go i tworzy na nowo, jeżeli nie zwyczajnie go tworzy. Trzeba mieć jednak na uwadze, że istnieje czas między sprawdzeniem warunku, a stworzeniem pliku plik.txt. Bezpieczeństwo kodu wymaga, aby nie zaistniała tzw. sytuacja wyścigu. Jest możliwe bowiem, by po sprawdzeniu warunku (zakładamy, że plik jeszcze nie istnieje) plik ten mógł zostać utworzony przez atakującego, co spowoduje "wykrzaczenie" się programu po wykonaniu komendy System.IO.File.Create("C:\\Temp\\plik.txt") (bo taki plik JUŻ został utworzony przez atakującego, przed wykonaniem tej procedury). Aby uniknąć takich sytuacji, niezbędna jest synchronizacja, której podstawy przedstawię poniżej.

Przejdziemy teraz do synchronizacji wątków za pomocą funkcji lock. Funkcja ta gwarantuje, że jeden wątek nie przejdzie do sekcji krytycznej kodu, podczas gdy inny jest w „swojej” sekcji krytycznej. Co więcej, w przypadku pracy z klasami, locki powinny być prywatne lub chronione. Dlaczego? Niektóre operacje muszą być wykonywane po kolei, nie mogą być równoległe. Spójrzmy na ten kawałek kodu:
       
Kod:
       private object toSync = new object();
        public void thr()
        {

            lock(toSync)
            {
                //sekcja krytyczna
            }

        }

…i ten:

       
Kod:
       private object toSync = new object();
       public void thr()
        {
            System.Threading.Monitor.Enter(toSync);

            try
            {
                //sekcja krytyczna
            }
            finally
            {
                System.Threading.Monitor.Exit(toSync);
            }
        }

są równoważne. Funkcja lock została stworzona jako referencja do klasy Monitor oraz niweluje potrzebę korzystania z bloku try. Teraz – do czego jest to potrzebne? Aby wątki nie przeszkadzały sobie wzajemnie. Wykonanie kodu w różnych wątkach operujących na tej samej zmiennej w pamięci może przynieść niepożądane skutki, takie jak „wykrzaczanie się” programu, co jest potencjalnym punktem zapalnym do exploitacji.
 
Teraz przejdźmy do niskopoziomowej synchronizacji, która jest jednak rzadko używana (lock daje nam podobne możliwości bez zbędnych „bajerów”). Mówię tu o Spinlockach czyli w wolnym tłumaczeniu kręcących się blokadach. SpinLock można przedstawić mniej więcej tak:

Kod:
while(!isHandleFree);

Wstrzymuje on działanie wątku tak długo, aż odpowiednia sekcja jest dostępna. Jest to zwyczajnie aktywna pętla sprawdzająca, czy wątek został zwolniony. SpinLocki używane są zazwyczaj tylko wtedy, gdy wydajność liczy się aż za bardzo. Są rodzajem pętli, która odpytuje, czy wątek jest zwolniony, i jeśli tak, wykonują kod. Jest to często szybsze, bo omija to serializację, nie ma potrzeby ingerowania w rejestry procesora. Trzeba mieć na uwadze to, że SpinLock nie jest klasą, lecz strukturą. Niżej podałem przykładowy kod z użyciem SpinLocka.

Kod:
using System;
using System.Threading;
using System.Threading.Tasks;

namespace test
{
    class Program
    {

        private static int _count;
        private static SpinLock _sLock = new SpinLock();

        static void Main(string[] args)
        {
            for (int i = 0; i != 10; i++)
            {
                var Thr = new Task(vInc);
                Thr.Start();
                Console.Write(_count + '\n');
            }
           Console.ReadKey();
        }

        static void vInc()
        {
            bool lockTaken = false;

            try
            {
                _sLock.Enter(ref lockTaken);
                _count++;
            }
            finally
            {
                if (lockTaken) _sLock.Exit();
            }
        }
    }
}

Przy tym, należy zaznaczyć, że Task jest asynchroniczny, a SpinLock właśnie go synchronizuje. I co ważne, przed wejściem w SpinLocka flaga lockTaken powinna być ustawiona na False.
Wady SpinLocka? Wciąż aktywny, wykonuje cykle, „siedzi” na procesorze. W przypadku długich operacji lock jest duży lepszy od jego niskopoziomowego odpowiednika.
0x DEADBEEF
Odpowiedz
#2
(09.04.2016, 20:37)chomikos napisał(a):

[Aby zobaczyć linki, zarejestruj się tutaj]

pewną klasę i strukturę
A co tutaj jest strukturą?
1. Zawsze mam rację.
2. Jeśli nie mam racji, patrz pkt 1.
Odpowiedz
#3
(09.04.2016, 20:48)Tajny Współpracownik napisał(a):

[Aby zobaczyć linki, zarejestruj się tutaj]

(09.04.2016, 20:37)chomikos napisał(a):

[Aby zobaczyć linki, zarejestruj się tutaj]

pewną klasę i strukturę
A co tutaj jest strukturą?
SpinLock jest strukturą w C#, nie klasą.
0x DEADBEEF
Odpowiedz
#4
A według mnie jest klasą.

Aaaaa już kumam. Mnie tylko uczyli struktur w C Sad
1. Zawsze mam rację.
2. Jeśli nie mam racji, patrz pkt 1.
Odpowiedz
#5
(09.04.2016, 20:58)Tajny Współpracownik napisał(a):

[Aby zobaczyć linki, zarejestruj się tutaj]

A według mnie jest klasą.

Aaaaa już kumam. Mnie tylko uczyli struktur w C Sad

Luzik Craze
0x DEADBEEF
Odpowiedz


Skocz do:


Użytkownicy przeglądający ten wątek: 1 gości