Chociaż mutex może być używany do rozwiązywania innych problemów, głównym powodem ich istnienia jest zapewnienie wzajemnego wykluczenia, a tym samym rozwiązanie tego, co jest znane jako stan rasy. Kiedy dwa (lub więcej) wątki lub procesy próbują uzyskać dostęp do tej samej zmiennej jednocześnie, mamy możliwość wystąpienia sytuacji wyścigu. Rozważmy następujący kod
//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
i++;
}
Elementy wewnętrzne tej funkcji wyglądają tak prosto. To tylko jedno stwierdzenie. Jednak typowym odpowiednikiem języka asemblera może być:
load i from memory into a register
add 1 to i
store i back into memory
Ponieważ wszystkie równoważne instrukcje języka asemblera są wymagane do wykonania operacji inkrementacji na i, mówimy, że inkrementacja i nie jest operacją atmosferyczną. Operacja atomowa to taka, którą można zakończyć na sprzęcie z gwarancją braku przerwania po rozpoczęciu wykonywania instrukcji. Przyrost i składa się z łańcucha 3 instrukcji atomowych. W systemie współbieżnym, w którym kilka wątków wywołuje funkcję, pojawiają się problemy, gdy wątek odczytuje lub zapisuje w niewłaściwym czasie. Wyobraź sobie, że mamy dwa wątki działające jednocześnie i jeden wywołuje funkcję bezpośrednio po drugim. Powiedzmy również, że zainicjowaliśmy i na 0. Załóżmy również, że mamy dużo rejestrów i że dwa wątki używają zupełnie innych rejestrów, więc nie będzie kolizji. Rzeczywisty czas tych wydarzeń może być:
thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1
Zdarzyło się, że mamy dwa wątki zwiększające się jednocześnie i, nasza funkcja jest wywoływana dwukrotnie, ale wynik jest niezgodny z tym faktem. Wygląda na to, że funkcja została wywołana tylko raz. Dzieje się tak, ponieważ atomowość jest „zepsuta” na poziomie maszyny, co oznacza, że wątki mogą się wzajemnie przerywać lub współpracować w niewłaściwych momentach.
Potrzebujemy mechanizmu, aby rozwiązać ten problem. Musimy trochę uporządkować powyższe instrukcje. Jednym z powszechnych mechanizmów jest blokowanie wszystkich wątków oprócz jednego. Mutex Pthread wykorzystuje ten mechanizm.
Każdy wątek, który musi wykonać pewne wiersze kodu, które mogą w niebezpieczny sposób modyfikować wspólne wartości przez inne wątki w tym samym czasie (używając telefonu do rozmowy z żoną), powinien najpierw uzyskać blokadę muteksu. W ten sposób każdy wątek wymagający dostępu do udostępnionych danych musi przejść przez blokadę mutex. Dopiero wtedy wątek będzie mógł wykonać kod. Ta sekcja kodu nazywana jest sekcją krytyczną.
Gdy wątek wykona sekcję krytyczną, powinien zwolnić blokadę muteksu, aby inny wątek mógł uzyskać blokadę muteksu.
Koncepcja posiadania muteksu wydaje się nieco dziwna, gdy rozważa się ludzi poszukujących wyłącznego dostępu do rzeczywistych, fizycznych obiektów, ale podczas programowania musimy być zamierzeni. Współbieżne wątki i procesy nie mają takiego społecznego i kulturowego wychowania, jak my, dlatego musimy zmusić je do ładnego udostępniania danych.
Więc technicznie rzecz biorąc, jak działa muteks? Czy nie cierpi z powodu tych samych warunków wyścigu, o których wspominaliśmy wcześniej? Czy pthread_mutex_lock () nie jest nieco bardziej skomplikowany niż zwykły przyrost zmiennej?
Technicznie rzecz biorąc, potrzebujemy wsparcia sprzętowego, aby nam pomóc. Projektanci sprzętu przekazują nam instrukcje maszynowe, które robią więcej niż jedną rzecz, ale gwarantują, że są atomowe. Klasycznym przykładem takiej instrukcji jest test-i-ustaw (TAS). Próbując uzyskać blokadę zasobu, możemy użyć TAS może sprawdzić, czy wartość w pamięci wynosi 0. Jeśli tak, to byłby to nasz sygnał, że zasób jest używany i nic nie robimy (lub dokładniej , czekamy według jakiegoś mechanizmu. Muteks pthreads umieści nas w specjalnej kolejce w systemie operacyjnym i powiadomi nas, gdy zasób stanie się dostępny. Głupsze systemy mogą wymagać od nas wykonania ciasnej pętli spinowej, testując stan w kółko) . Jeśli wartość w pamięci nie jest równa 0, TAS ustawia lokalizację na inną niż 0 bez używania innych instrukcji. To' to jak połączenie dwóch instrukcji asemblacji w 1, aby uzyskać atomowość. Zatem testowanie i zmiana wartości (jeśli zmiana jest odpowiednia) nie może zostać przerwana po rozpoczęciu. Na podstawie takiej instrukcji możemy zbudować muteksy.
Uwaga: niektóre sekcje mogą wyglądać podobnie do wcześniejszej odpowiedzi. Przyjąłem jego zaproszenie do redagowania, wolał oryginalny sposób, więc zachowuję to, co miałem, co jest nasycone odrobiną jego słownictwa.