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. System operacyjny dba o synchronizację, jeśli więcej wątków żąda wykonania kodu sekcji krytycznej, dopuszczany jest tylko jeden wątek, pozostałe są wstrzymywane. Dąży się do tego, aby kod sekcji krytycznej był krótki – wykonywał się szybko. Poprzez ostrożne kontrolowanie, które zmienne są modyfikowane wewnątrz i poza sekcją krytyczną (zwykle poprzez uzyskiwanie dostępu do istotnego stanu tylko z zewnątrz), zapobiega się współbieżnemu dostępowi do tego stanu. Sekcja krytyczna jest zwykle używana, kiedy program wielowątkowy musi uaktualnić wiele powiązanych zmiennych, tak żeby inny wątek nie dokonał szkodliwych zmian tych danych. W odnośnej sytuacji, sekcja krytyczna może być użyta, żeby zagwarantować, że wspólny zasób – np. drukarka – jest używana tylko przez jeden proces w określonym czasie.

Sposób implementacji sekcji krytycznych jest zależny od systemu operacyjnego.

Najprostszą metodą jest zapobieżenie jakiejkolwiek zmianie kontroli procesora w obrębie sekcji krytycznej. W systemach z jednym procesorem może to zostać zrealizowane poprzez uniemożliwienie przerwań na wejściu do sekcji krytycznej, unikanie wywołań systemowych, które mogą spowodować przełączanie kontekstu w obrębie sekcji i przywracanie przerwań do ich poprzedniego stanu na wyjściu. Każdy wątek egzekucji wchodzący do dowolnej sekcji krytycznej gdziekolwiek w systemie, dzięki tej implementacji, uniemożliwi jakiemukolwiek innemu wątkowi, włącznie z przerwaniem, uzyskanie dostępu do procesora, a co za tym idzie, wejście do jakiejkolwiek innej sekcji krytycznej (a nawet dostęp do jakiegokolwiek kodu), dopóki oryginalny wątek nie opuści sekcji krytycznej.

Takie siłowe podejście może zostać rozwinięte przez użycie semaforów. Aby wejść do sekcji krytycznej, wątek musi zdobyć semafor, który uwalnia opuszczając sekcję. Inne wątki nie mogą wejść do sekcji krytycznej w tym samym czasie co oryginalny wątek, ale mogą uzyskać kontrolę nad procesorem i wykonać inny kod, włącznie z innymi sekcjami krytycznymi, które są chronione przez inne semafory.

Istnieją w literaturze pewne niejasności co do relacji pomiędzy różnymi sekcjami krytycznymi w tym samym programie. Zasadniczo, zasób, który musi być chroniony przed współbieżnym dostępem, może być dostępny z różnych fragmentów kodu. Każdy fragment musi być chroniony przez wspólny semafor. Czy teraz każdy fragment stanowi sekcję krytyczną, czy też wszystkie fragmenty chronione przez ten sam semafor stanowią jedną sekcję krytyczną? Ta niejasność jest widoczna w definicjach sekcji krytycznej takich jak „...fragment kodu, który może być wykonywany tylko przez jeden proces lub wątek jednocześnie.” Ta definicja jest prawdziwa tylko wtedy, gdy cały dostęp do chronionego zasobu jest zawarty w jednym fragmencie kodu, co wymaga albo definicji fragmentu kodu, albo tego, by sam kod był poniekąd sztuczny.

Sekcje krytyczne realizuje się np. z wykorzystaniem muteksów lub semaforów. Ponadto systemy operacyjne posiadają zwykle specjalne obiekty do tego rodzaju synchronizacji w obrębie wątków jednego procesu.

Brak wzajemnego wykluczania się wykonywania sekcji krytycznych może spowodować błędy wykonania, np. dwukrotne zapisanie danej albo niepoprawna modyfikacja zasobu (patrz poniższy przykład).

Konsekwencja braku synchronizacji

Rozpatrzmy dwa procesy (P1, P2), które po wykonaniu zadania zwiększają pewien globalny licznik. Ponadto każdy proces dysponuje swoją lokalną pamięcią, niedostępną dla pozostałych.

Zwiększenie licznika jest realizowane w trzech krokach:

  1. skopiowanie aktualnej wartości licznika do lokalnej zmiennej,
  2. zwiększenie tej wartości,
  3. zapisanie nowej wartości do globalnego licznika.

Program, który to wykonuje, składa się z następujących kroków:

1: x = licznik
2: x = x + 1
3: licznik = x

Poszczególne kroki jako takie są niepodzielne (atomowe), lecz wykonanie składającego się z nich programu może być w dowolnej chwili przerwane.

procesdziałanielicznikx1x2komentarz
1??sytuacja początkowa, licznik zainicjowany na wartość 1
P1x1 = licznik11?system operacyjny przekazuje sterowanie do P1, P1 wykonuje pierwszą instrukcję
P1x1 = x1 + 112?P1 kontynuuje działanie
P2x2 = licznik121system operacyjny przełącza kontekst na P2, P2 wykonuje pierwszą instrukcję
P1licznik = x1221system operacyjny z powrotem przełącza kontekst na P1, P1 kończy wykonywanie na aktualizacji globalnego licznika
P2x2 = x2 + 1222system operacyjny z powrotem przełącza kontekst na P2, który kontynuuje działanie
P2licznik = x2222P2 kończy działanie i aktualizuje globalny licznik błędną wartością

Powód błędu: oba procesy posługują się aktualną wartością licznika przy obliczeniach, nie uwzględniono sytuacji (która w tym przykładzie zaistniała), że zawartość licznika zmienia się w trakcie obliczeń.

Przy prawidłowym wykonywaniu tego kodu, to znaczy zagwarantowaniu sekcją krytyczną, że te trzy instrukcje wykonają się w całości wyłącznie w kontekście jednego procesu, końcową wartością licznika byłoby spodziewane 3.

procesdziałanielicznikx1x2komentarz
1??sytuacja początkowa, licznik zainicjowany na wartość 1
P1x1 = licznik11?system operacyjny przełącza kontekst na P1, P1 wykonuje pierwszą instrukcję
x1 = x1 + 112?P1 kontynuuje działanie
licznik = x1221P1 kończy wykonywanie na aktualizacji globalnego licznika
P2x2 = licznik222system operacyjny ustawia kontekst na P2, P2 wykonuje pierwszą instrukcję
x2 = x2 + 1223P2 kontynuuje działanie
licznik = x2323P2 kończy działanie i aktualizuje globalny licznik tym razem poprawną wartością

Sekcje krytyczne poziomu aplikacji

Sekcje krytyczne poziomu aplikacji rezydują w zakresie pamięci procesu i są zwykle modyfikowalne przez sam proces. Nazywa się to obiektem przestrzeni użytkownika, ponieważ program uruchamiany przez użytkownika (w przeciwieństwie do jądra) może modyfikować obiekt i wchodzić z nim w interakcję. Jednakże wywołane funkcje mogą przeskoczyć do przestrzeni jądra aby zarejestrować w nim obiekt przestrzeni użytkownika.

Przykładowy kod sekcji krytycznych z wykorzystaniem POSIXowej biblioteki pthread

/* Przykład C/C++, Unix/Linux */
#include <pthread.h>

/* To jest obiekt sekcji krytycznej (statycznie zaalokowany) */
static pthread_mutex_t cs_mutex = PTHREAD_MUTEX_INITIALIZER;

void f()
{
    /* Wejdź do sekcji krytycznej – inne wątki są zablokowane*/
    pthread_mutex_lock( &cs_mutex );
 
    /* Wykonaj trochę bezpiecznego przetwarzania*/

    /*Opuść sekcję krytyczną – inne wątki mogą teraz pthread_mutex_lock()  */
    pthread_mutex_unlock( &cs_mutex );
}

Przykładowy kod sekcji krytycznych z API Win32

/* Przykład C/C++, Windows, link do kernel32.dll */
#include <windows.h>

static CRITICAL_SECTION cs; /* To jest obiekt sekcji krytycznej – raz zainicjalizowany nie może być przesunięty w pamięci*/
                            /* Jeżeli programujesz w językach obiektowych, zadeklaruj to jako niestatyczną składową twojej klasy */
 
/* Zainicjalizuj sekcję krytyczną przed wejściem do wielowątkowego kontekstu */
InitializeCriticalSection(&cs);

void f()
{
    /* Wejdź do sekcji krytycznej – inne wątki są zablokowane */
    EnterCriticalSection(&cs);

    /* Wykonaj trochę bezpiecznego przetwarzania*/

   /*Opuść sekcję krytyczną – inne wątki mogą teraz EnterCriticalSection() */
    LeaveCriticalSection(&cs);
}

/* Uwolnij obiekt systemowy po zakończeniu wszystkiego – zwykle na końcu kodu czyszczącego */
DeleteCriticalSection(&cs);

Należy zauważyć, że w Windowsie NT (nie 9x/ME), funkcja TryEnterCriticalSection() może być użyta do podjęcia próby wejścia do sekcji krytycznej. Ta funkcja zwraca wynik natychmiast, tak że wątek może robić inne rzeczy, jeśli nie uda mu się dostać do sekcji krytycznej (zwykle ze względu na to, że inny wątek ją zablokował). W bibliotece pthreads odpowiadającą funkcją jest pthread_mutex_trylock(). Należy zauważyć, że użycie CriticalSection nie jest tym samym co Mutex w Win32, który jest obiektem używanym do międzyprocesowej synchronizacji. CriticalSection w Win32 służy do wewnątrzprocesowej synchronizacji (i jest znacznie szybszy, jeśli chodzi o czasy blokowania), ale nie może być dzielony pomiędzy procesami.

Zobacz też