Nastoletni
Programiści

Logo Nastoletnich Programistów

Synchronizacja wątków w C# i C++ (Spinlock)

Wielowątkowość, synchronizacja… Ale po co to komu?

Przytoczę pewne bardzo obrazowe porównanie. Wyobraź sobie, że jesteś w restauracji. Podnosisz menu, wybierasz potrawę i czekasz na kelnera. Widzisz, że tymczasowo obsługuje innego klienta. Mija kilka, kilkanaście minut, a kelner wciąż stoi przy stoliku innego klienta, obsługuje tylko jego. Dopiero, gdy klient płaci i wychodzi z restauracji, kelner podchodzi do Ciebie i zaczyna cię obsługiwać. W tym czasie przychodzi inny klient, ale kelner jest zajęty tylko tobą. Mniej więcej tak działa aplikacja jednowątkowa (nasz kelner). W oczywisty sposób jest to nieefektywne.

Wtedy niczym superbohater, przybywa wielowątkowość.

Super! A więc zyskujemy trochę wydajności. Jednak rozważmy ten kod w języku C# i przeanalizujmy go.

Co tu się dzieje? Program sprawdza czy plik examplefile.txt istnieje, jeżeli nie, tworzy go. Teraz przejdźmy do tego, dlaczego kod ten nie jest bezpieczny. Jako, że dziś mamy do czynienia z wieloma procesorami (rdzeniami), to w tle mogą działać równocześnie różne programy. Nie ma więc żadnej gwarancji, że jeden z nich nie utworzy pliku o nazwie examplefile.txt właśnie w tym katalogu. Rozchodzi się więc o moment pomiędzy otrzymaniem informacji, że plik nie istnieje, a utworzeniem go. Klasa tego błędu to tzw. Race Condition. Aby uniknąć takich sytuacji, wymagana jest synchronizacja.

Najprostszym używanym niskopoziomowym mechanizmem do synchronizacji są tzw. Spinlocki (w luźnym tłumaczeniu – kręcące się blokady). Jest on w zasadzie aktywną pętlą, która czeka, aż dany obiekt (blokada) zostanie zwolniony. Dziś w zasadzie się z nich nie korzysta, gdyż nowoczesne języki takie jak C# mają wbudowane mechanizmy synchronizujące, takie jak lock. Lock działa w ten sposób:

i jest równoważny temu:

gdyż lock jest w zasadzie aliasem tego drugiego. Teraz, zanim przejdę do spinlocków, wytłumaczę z pomocą Wikipedii czym jest owa sekcja krytyczna.

Sekcja krytyczna – fragment kodu programu, w którym korzysta się z zasobu dzielonego, a co za tym idzie w danej chwili może być wykorzystywany przez co najwyżej jeden wątek.

Myślę, że ta definicja spokojnie wystarczy. Przejdźmy teraz do Spinlocków.

W C++ można podejść do tego na prawdę prosto, jest to zwyczajnie (jak już pisałem) aktywna pętla. Wygląda to mniej więcej tak (korzystając z biblioteki <atomic> służącej właśnie do synchronizacji, i jak sama nazwa mówi do operacji atomowych):

Dzięki Spinlockom możemy także zsynchronizować uruchomienie wątków, aby wystartowały w mniej więcej tym samym czasie, co można zaimplementować bardzo prosto:

Wówczas nasza blokada może blokować(?) wykonanie wątku tak długo, jak programista sobie zażyczy. Słówka volatile użyłem, gdyż nie chce, by kompilator potraktował tę pętle tak:
if (slock != THREAD_COUNT) while(1); a tego oczywiście nie chcemy.

C# również udostępnia nam mechanizm spinlocków, ba, w zasadzie całą strukturę! Napisałem tu krótki program implementujący Spinlock:

Przeanalizujmy, co się w nim dzieje.


Zacznijmy tuż przed punktem wejścia do programu, tj. static void Main(string[] args). Tworzymy w nim instancję struktury SpinLock, w bardzo standardowy sposób, oraz prywatne pole int o nazwie _count, którego zadaniem będzie przechowywanie pewnej wartości. Wewnątrz funkcji głównej tworzę prostą pętlę, w niej tworze natomiast nowy obiekt Task, który będzie asynchronicznie wykonywał naszą funkcję vInc(). Tam widzimy standardowy blok try finally,  w którym to rozpoczynamy spin SpinLocka. Zauważ, że zanim wszedłem w aktywną pętlę, flagę lockTaken ustawiłem na false i jest przekazywana przez referencję, nie wartość.

To by było na tyle, jeżeli czegoś wystarczająco nie rozwinąłem, albo macie jakieś pytania, zachęcam do komentowania!


Źródła

 

camed

16-latek uwielbiający C#, Security IT, algorytmikę, TF2, piłkę siatkową i nożną. Będzie próbować swoich sił w OI. Dodatkowo amator książek fantasy.

Zobacz wszystkie posty tego autora →

Komentarze