# L9 - GRUPA 1 ## Zadanie 1 :::success Autor: Maksymilian Komarnicki ::: _Lokalność czasowa_ Komórki pamięci, które były ostatnio używane będą z wysokim prawdopodobieństwem używane ponownie w niedalekiej przyszłości. _Lokalność przestrzenna_ Komórki pamięci o pobliskich adresach mają tendencje do bycia używanymi w krótkich odstępach czasu. (Lokalność czasowa) Pamięci podręczne wykorzystują lokalność odwołań poprzez pamiętanie w liniach komórek pamięci, które były ostatnio używane. Dzięki temu ponowny dostęp do tych samych danych jest bardzo szybki, o ile dane nie zostały usunięte z pamięci. (Lokalność przestrzenna) Pamięci podręczne wykorzystują także lokalność poprzez umieszczanie linii o pobliskich adresach w różnych zbiorach pamięci podręcznej, co umożliwia trzymanie w niej większego kawałka spójnej pamięci. Procesor A i B posiadają swoje pamięci podręczne. Przypuśćmy, że oba wczytały dane pod adresem X do swoich pamięci podręcznych. Jeśli jeden z procesorów dokona modyfikacji tej danej, to pamięci podręczne procesorów A i B przestaną być spójne. Z tego względu potrzebne są protokoły spójności pamięci podręcznej. _MESI_, czyli każda z linii pamięci podręcznej może byc oznaczona jako - Modified - dane pamięci podręcznej zostały zmienione, muszą zostać zapisane do pamięci operacyjnej, - Exclusive - dane niemodyfikowane, jedyna kopia w jakiejkolwiek pamięci podręcznej, - Shared - dane niemodyfikowane, mogą być też w innej pamięci podręcznej, - Invalid - zawartość pamięci podręcznej nie jest spójna/zgodna. ## Zadanie 2 :::success Autor: Wojciech Pokój ::: :::info W jaki sposób mierzy się wydajność implementacji zamków? ::: Układa się prosty program z krótką sekcją krytyczną (na przykład licznik) i mierzy się czas wykonania programu i średni czas potrzeby na zajęcie zamka :::info Wyjaśnij, skąd bierze się różnica w wydajności zamka TAS vs. TTAS odwołując się do modelu komunikacji w systemach wieloprocesorowych ze spójnymi pamięciami podręcznymi i wspólną szyną danych. ::: Różnica wynika z konstrukcji pętli czekającej. TAS aktywnie zagląda do pamięci współdzielonej, przez co szyna komunikacyjna staje się obciążona. TTAS wykorzystuje fakt że pamięć znajduje się w cachu i dopuki nie zmieni stanu nie trzeba sięgać do pamięci współdzielonej ## Zadanie 3 :::success Autor: Marcin Sarnecki ::: ![](https://i.imgur.com/uQzxAUG.png) * Przypomnij zasadę działania i podaj implementację zamka kolejkowego Andersona Mamy współdzieloną tablicę zmiennych boolowskich (flag), na poczatku wszedzie są wartości `false` z wyjatkiem poczatku, mamy współdzieloną zmienną `next`. Aby wziąć zamek, wątek atomowo wykonuje `getAnIncrement()` na zmiennej `next`, nastepnie czeka, aż nie pojawi sie tam wartosc `true`, stanie się to gdy poprzedni wątek wykona `unlock()` ![](https://i.imgur.com/RjtPvlR.png) ![](https://i.imgur.com/SkDCsht.png) * Jakie zalety ma ten zamek w stosunku do zamków TAS/TTAS/Backoff? ![](https://i.imgur.com/3dNDGXj.png) * Czego dotyczy problem fałszywego współdzielenia wierszy pamięci podręcznej (ang. false sharing) w tym algorytmie i jak go rozwiązać? Kolejne komórki z tablicy `flags` będą znajdowały się w tych samych wierszach pamięci, każda zmiana pojedynczej flagi spowoduje zmianę stanu całego wiersza, zatem będzie więcej niepotrzebnego współdzielenia. ![](https://i.imgur.com/Z0TxcuO.png) ![](https://i.imgur.com/qWkIcvQ.png) Rozwiązaniem jest takie rozmieszczenie flag w tablicy, aby na jedną flagę przypadał jeden wiersz pamięci. ## Zadanie 4 :::success Autor: Mikołaj Depta ::: ![](https://i.imgur.com/6GqzhlT.png) ![](https://i.imgur.com/0Z9GUGx.png) W pierszym podejściu wiele wątków aktywnie modyfikuje i odczytuje wartość jedenj, komórki. Będzie to przyczyną ciągłych inwalidacji pamięci cache i wymuszenie odczytu z pamieci głównej, co jest bardzo kosztowne. W drugim przypadku mamy podejście podobne do algorytmu Anderson'a, gdzie każdy wątek aktywnie czeka i pisze tylko do jendej komórki, poza ostatnią. Pomijając aspekt kolizji linii cache'u, który można objeść, inwalidajce pamięci podręcznej wystąpią nie więcej niż dwa razy dla każdego wątku. Raz gdy wątek `i - 1` naszpie do swojej komóki i ewentuanie gdy `n`'ty wątek napisze do swojej. Druga implementacja powinna być dużo szybsza, ponieważ generuje stałą liczbę inwalidacji per wątek, a nie jak w pierwszym przypadku proporcjonalnie do ilości wątków. ## Zadanie 5 :::success Autor: Tomasz Wołczański ::: ![](https://i.imgur.com/g8tIyHy.png) W zamku CLH, podobnie jak w zamku Andersona, każdy wątek oczekuje w pętli na zmianę wartości komórki pamięci, która może zostać zmodyfikowana tylko przez jeden wątek. Dzięki temu liczba niepotrzebnych dostępów do szyny danych jest minimalna i narzut czasowy związany z rywalizacją wątków jest prawie zerowy. W przeciwieństwie do zamka Andersona, zamek CLH wykorzystuje (niejawną) listę wiązaną, więc osiąga lepszą złożoność pamięciową. Gdy w systemie mamy $L$ zamków i maksymalnie $n$ wątków, to algorytm Andersona musi zaalokować tablicę rozmiaru $n$ dla każdego zamka, co daje złożoność pamięciową $O(nL$). Natomiast złożoność algorytmu CLH wynosi $O(n+L)$. W algorytmie CLH wątek, który chce uzyskać dostęp do sekcji krytycznej tworzy węzeł `QNode` i inicjalizuje jego wartość na `true` (jeśli wątek nie uzyskał jeszcze dostępu do sekcji krytycznej, to wartość w jego węźle jest równa `true`, w przeciwnym razie `false`). Następnie przy pomocy metody `getAndSet` atomowo odczytuje i ustawia ogon niejawnej listy na swój węzeł. Jeśli wartość w odczytanym ogonie jest równa `true`, to znaczy, że w sekcji krytycznej znajduje się jeszcze jakiś wątek, więc trzeba na niego poczekać. Zwolnienie zamka przez wątek polega na ustawieniu wartości w węźle tego wątku na `false`. Zamek można zaimplementować w następujący sposób: ```java= public class CLHLock implements Lock { AtomicReference<QNode> tail; ThreadLocal<QNode> myNode; public CLHLock() { tail = new AtomicReference<QNode>(new QNode()); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; } public void lock() { QNode qnode = new QNode(); qnode.locked = true; myNode.set(qnode); QNode pred = tail.getAndSet(qnode); while (pred.locked) {} } public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode = null; } class QNode { volatile boolean locked = false; } } ``` Aby ograniczyć liczbę alokacji węzłów `QNode`, można w metodzie `unlock` ustawiać węzeł `myNode` na węzeł występujący przed nim na liście - wątek odpowiadający temu węzłowi wyszedł już z sekcji krytycznej, więc ten węzeł jest nieużywany. ```java= public class CLHLock implements Lock { AtomicReference<QNode> tail; ThreadLocal<QNode> myPred; ThreadLocal<QNode> myNode; public CLHLock() { tail = new AtomicReference<QNode>(new QNode()); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; myPred = new ThreadLocal<QNode>() { protected QNode initialValue() { return null; } }; } public void lock() { QNode qnode = myNode.get(); qnode.locked = true; QNode pred = tail.getAndSet(qnode); myPred.set(pred); while (pred.locked) {} } public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode.set(myPred.get()); } class QNode { volatile boolean locked = false; } } ``` ## Zadanie 6 :::success Autor: Marcin Wróbel ::: ![](https://i.imgur.com/S7xTFyE.png) ```java public class BadCLHLock implements Lock { AtomicReference<Qnode> tail = new AtomicReference<QNode>(new QNode()); ThreadLocal<Qnode> myNode = new ThreadLocal<QNode> { protected QNode initialValue() { return new QNode(); } }; public void lock() { Qnode qnode = myNode.get(); qnode.locked = true; // I’m not done // Make me the new tail, and find my predecessor Qnode pred = tail.getAndSet(qnode); while (pred.locked) {} } public void unlock() { // reuse my node next time myNode.get().locked = false; } static class Qnode { // Queue node inner class volatile boolean locked = false; } } ``` Implementacja jest błędna, ponieważ może dojść do zakleszczenia. Niech `X_myNode(y)` oznacza, że zmienna `myNode` wątku `X` jest obiektem `QNode` z wartością `locked` równą `y`; Działa tylko jeden wątek nazwany A. - A.lock() Po wykonaniu `tail == A_myNode(true)` - A.unlock() Po wykonaniu `tail == A_myNode(false)` - A.lock() `qnode.locked = true;` sprawi, że `tail == A_myNode(true)` `Qnode pred = tail.getAndSet(qnode);` po tej instrukcji `pred == A_myNode(true)` Program w nieskończoność będzie wykonywał pętlę `while (pred.locked) {}`, choć jest jedynym wątkiem chcącym uzyskać zamek. ## Zadanie 7 :::success Autor: Jan Dalecki ::: ![](https://i.imgur.com/BPmxtMp.png) ![](https://i.imgur.com/b5ejaw0.png) ![](https://i.imgur.com/PbyvQ1I.png) Zamek implementuje timeout poprzez metodę `tryLock`, którą może wywołać każdy wątek w bloku try...catch. Metoda przyjmuje jako argument czas, który wątek wywołujący jest w stanie czekać na dostanie zamka. Próba otrzymania zamka jest porzucana jeżeli minie okrelony czas. W algorytmnie CLH zwykłe porzucenie próby dostania się do zamka spowodowałoby zagłodzenie wątków czekających w kolejce. Z tego powodu porzucone węzły są przejmowane przez kolejne przychodzące wątki. W strukturze `QNode` przechowujemy wskaźnik pod polem `pred` na inny `QNode`. Pole `pred` jest na początku zawsze `null`, a jeżeli nie zostanie podmienione na strukturę `AVAILABLE` tylko na inną strukturę, to oznacza, że wątek z niepowodzeniem wykonał metodę `tryLock`. Jeżeli poprzednik (wskazywany przez `tail`) miał wartość `null` lub przechowywał `AVAILABLE` możemy otrzymać zamek. W przeciwnym przypadku wchodzimy w pętlę na ustalony przez nas czas. W pętli czekamy, aż pole `pred` poprzedniego wątku stanie się `AVAILABLE` - wtedy dostajemy zamek. Jeżeli zmieni się na inną wartość (poprzednik porzucił próbę dostania się do zamka) zmieniamy naszego poprzednika na tego, który jest wskazywany przez `pred`. Jeżeli minie nasz czas musimy ustawić `tail` na naszego poprzednika, jeżeli jesteśmy ostatni w kolejce (`tail` wskazuje na nas). W przeciwnym przypadku ustawiamy nasze pole `pred` na naszego poprzednika. ![](https://i.imgur.com/NiKL0x8.jpg) ![](https://i.imgur.com/SjIzMYN.jpg) ## Zadanie 8 :::success Autor: Wiktor Bukowski ::: ![](https://i.imgur.com/N2qVtdW.png) ![](https://i.imgur.com/q64cdqg.png) Zamek opiera się na następującej idei: Gdy zamek próbuje zająć blokadę, ustawia się na końcu kolejki. Jeśli jest już tam jakiś wątek, modyfikuje jego wskaźnik `next` na samego siebie, a następnie czeka aż ten wątek ustawi jego zmienną `locked` na fałsz. Aby zwolnić zamek musimy sprawdzić, czy żaden wątek nie ustawił się za nami. Sprawdzamy więc swój wskaźnik `next`. Jeśli nie jest nullem, ustawiamy jego zmienną `locked` na fałsz i kończymy algorytm. Jeśli jednak jest nullem, możliwym jest powolne ustawianie tego wskaźnika przez inny wątek. Aby wyeliminować ten problem, próbujemy ustawić zmienną `tail` (trzymającą koniec kolejki) na nulla. Jeśli się to uda, wiemy, że jesteśmy jedynym wątkiem i możemy zakończyć algorytm. Jeśli się to nie uda, to inny wątek powinien zaraz zmodyfikować nasz wskaźnik `next`, więc czekamy na to i ustawiamy jego zmienną `locked` na fałsz. Po wszystkich tych krokach żaden wątek nie może już skorzystać z naszego węzła, a więc możemy go użyć ponownie. W systemie o architekturze NUMA wydajność tego zamka jest satysfakcjonująca, ponieważ każdy z wątków pętli się na komórce znajdującej się w jego własnej pamięci. ## Zadanie 9 :::success Autor: Mateusz Kisiel ::: ![](https://i.imgur.com/xcvMLcI.png) ### TAS ```java= class TASlock { AtomicBoolean state = new AtomicBoolean(false); void lock() { while (state.getAndSet(true)) {} } void unlock() { state.set(false); } public boolean isLocked(){ return state.get(); } } ``` ### CLH ```java public class CLHLock implements Lock { AtomicReference<QNode> tail; ThreadLocal<QNode> myPred; ThreadLocal<QNode> myNode; public CLHLock() { tail = new AtomicReference<QNode>(new QNode()); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; myPred = new ThreadLocal<QNode>() { protected QNode initialValue() { return null; } }; } public void lock() { QNode qnode = myNode.get(); qnode.locked = true; QNode pred = tail.getAndSet(qnode); myPred.set(pred); while (pred.locked) {} } public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode.set(myPred.get()); } public boolean isLocked(){ return tail.get().locked; } class QNode { volatile boolean locked = false; } } ``` ### MCS ```java= public class MCSLock implements Lock { AtomicReference<QNode> tail; ThreadLocal<QNode> myNode; public MCSLock() { tail = new AtomicReference<QNode>(null); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; } public void lock() { QNode qnode = myNode.get(); QNode pred = tail.getAndSet(qnode); if (pred != null) { qnode.locked = true; pred.next = qnode; // wait until predecessor gives up the lock while (qnode.locked) {} } } public void unlock() { QNode qnode = myNode.get(); if (qnode.next == null) { if (tail.compareAndSet(qnode, null)) return; // wait until successor fills in its next field while (qnode.next == null) {} } qnode.next.locked = false; qnode.next = null; } public boolean isLocked(){ QNode pred = tail.get(); return pred != null; /// kolejka jest pusta } class QNode { volatile boolean locked = false; volatile QNode next = null; } } ```