Żadna z podstawowych struktur danych nie jest bezpieczna wątkowo. Jedyne, co wiem, że jest dostarczane z Rubim, to implementacja kolejki w standardowej bibliotece ( require 'thread'; q = Queue.new
).
GIL MRI nie chroni nas przed problemami z bezpieczeństwem wątków. To daje pewność, że tylko dwa wątki nie można uruchomić kod Ruby w tym samym czasie , tj. Na dwóch różnych procesorach w tym samym czasie. Wątki można nadal wstrzymywać i wznawiać w dowolnym momencie w kodzie. Jeśli piszesz kod, taki jak @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
np. Mutowanie wspólnej zmiennej z wielu wątków, późniejsza wartość wspólnej zmiennej nie jest deterministyczna. GIL jest mniej więcej symulacją systemu jednordzeniowego, nie zmienia podstawowych kwestii pisania poprawnych programów współbieżnych.
Nawet gdyby MRI był jednowątkowy, jak Node.js, nadal musiałbyś pomyśleć o współbieżności. Przykład ze zmienną inkrementowaną działałby dobrze, ale nadal można uzyskać warunki wyścigu, w których rzeczy dzieją się w niedeterministycznej kolejności, a jedno wywołanie zwrotne przeskakuje wynik innego. Jednowątkowe systemy asynchroniczne są łatwiejsze do rozważenia, ale nie są wolne od problemów ze współbieżnością. Wystarczy pomyśleć o aplikacji z wieloma użytkownikami: jeśli dwóch użytkowników kliknie edycję w poście Stack Overflow mniej więcej w tym samym czasie, poświęć trochę czasu na edycję posta, a następnie kliknij Zapisz, którego zmiany zobaczy trzeci użytkownik później, gdy czytać ten sam post?
W Rubim, podobnie jak w większości innych współbieżnych środowisk wykonawczych, wszystko, co jest więcej niż jedną operacją, nie jest bezpieczne dla wątków. @n += 1
nie jest bezpieczny wątkowo, ponieważ jest to wiele operacji. @n = 1
jest bezpieczna dla wątków, ponieważ jest to jedna operacja (jest wiele operacji pod maską i prawdopodobnie wpadłbym w kłopoty, gdybym próbował szczegółowo opisać, dlaczego jest "bezpieczny wątkowo", ale ostatecznie nie uzyskasz niespójnych wyników z przypisań ). @n ||= 1
, nie jest i żadna inna skrócona operacja + przypisanie też nie jest. Jeden błąd, który popełniałem wiele razy, to pisanie return unless @started; @started = true
, które wcale nie jest bezpieczne dla wątków.
Nie znam żadnej autorytatywnej listy bezpiecznych dla wątków i nie-wątkowych instrukcji dla Rubiego, ale jest prosta zasada: jeśli wyrażenie wykonuje tylko jedną operację (bez skutków ubocznych), prawdopodobnie jest bezpieczne dla wątków. Na przykład: a + b
jest w porządku, a = b
jest również w porządku i a.foo(b)
jest w porządku, jeśli metoda foo
jest wolna od skutków ubocznych (ponieważ prawie wszystko w Rubim jest wywołaniem metody, w wielu przypadkach nawet przypisanie, dotyczy to również innych przykładów). Skutki uboczne w tym kontekście oznaczają rzeczy, które zmieniają stan. niedef foo(x); @x = x; end
jest wolny od skutków ubocznych.
Jedną z najtrudniejszych rzeczy w pisaniu kodu bezpiecznego dla wątków w Rubim jest to, że wszystkie podstawowe struktury danych, w tym tablica, hash i string, są modyfikowalne. Bardzo łatwo jest przypadkowo ujawnić część swojego stanu, a kiedy ten element jest zmienny, rzeczy mogą się naprawdę schrzanić. Rozważ następujący kod:
class Thing
attr_reader :stuff
def initialize(initial_stuff)
@stuff = initial_stuff
@state_lock = Mutex.new
end
def add(item)
@state_lock.synchronize do
@stuff << item
end
end
end
Instancja tej klasy może być współużytkowana między wątkami i mogą bezpiecznie dodawać do niej rzeczy, ale występuje błąd współbieżności (nie jedyny): stan wewnętrzny obiektu przecieka przez stuff
akcesor. Oprócz tego, że jest problematyczny z punktu widzenia enkapsulacji, otwiera również puszkę robaków współbieżnych. Może ktoś weźmie tę tablicę i przekaże ją gdzie indziej, a ten kod z kolei myśli, że teraz jest właścicielem tej tablicy i może z nią zrobić, co chce.
Innym klasycznym przykładem Rubiego jest:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
działa dobrze przy pierwszym użyciu, ale zwraca coś innego za drugim razem. Czemu? load_things
Metoda zdarza się myśleć, że jest właścicielem hash opcji przekazaną do niej i robi color = options.delete(:color)
. TerazSTANDARD_OPTIONS
stała nie ma już tej samej wartości. Stałe są stałe tylko w tym, do czego się odnoszą, nie gwarantują one stałości struktur danych, do których się odnoszą. Pomyśl tylko, co by się stało, gdyby ten kod był uruchamiany jednocześnie.
Jeśli unikniesz współdzielonego mutowalnego stanu (np. Zmiennych instancji w obiektach, do których uzyskuje dostęp wiele wątków, struktur danych, takich jak skróty i tablice, do których uzyskuje dostęp wiele wątków), bezpieczeństwo wątków nie jest takie trudne. Spróbuj zminimalizować części aplikacji, do których można uzyskać dostęp jednocześnie, i skup się na nich. IIRC, w aplikacji Railsowej, dla każdego żądania tworzony jest nowy obiekt kontrolera, więc będzie używany tylko przez jeden wątek i to samo dotyczy wszystkich obiektów modelu, które utworzysz z tego kontrolera. Jednak Railsy zachęcają również do używania zmiennych globalnych ( User.find(...)
używa zmiennej globalnejUser
, możesz myśleć o tym jak o tylko klasie i jest to klasa, ale jest to również przestrzeń nazw dla zmiennych globalnych), niektóre z nich są bezpieczne, ponieważ są tylko do odczytu, ale czasami zapisujesz rzeczy w tych zmiennych globalnych, ponieważ jest wygodny. Zachowaj ostrożność, używając wszystkiego, co jest dostępne na całym świecie.
Uruchamianie Railsów w środowiskach wątkowych jest możliwe już od dłuższego czasu, więc nie będąc ekspertem od Railsów, posunąłbym się nawet do stwierdzenia, że nie musisz martwić się o bezpieczeństwo wątków, jeśli chodzi o same Railsy. Nadal możesz tworzyć aplikacje Railsowe, które nie są bezpieczne dla wątków, robiąc niektóre z rzeczy, o których wspomniałem powyżej. Jeśli chodzi o inne klejnoty, zakładają, że nie są bezpieczne dla wątków, chyba że mówią, że są, a jeśli mówią, że są, zakładają, że tak nie jest, i przejrzyj ich kod (ale tylko dlatego, że widzisz, że działają na przykład@n ||= 1
nie oznacza, że nie są one bezpieczne dla wątków, jest to całkowicie uzasadnione we właściwym kontekście - zamiast tego należy szukać rzeczy takich jak zmienny stan w zmiennych globalnych, jak obsługuje on zmienne obiekty przekazane do swoich metod, a zwłaszcza jak to obsługuje skróty opcji).
Wreszcie, brak bezpieczeństwa wątku jest właściwością przechodnią. Wszystko, co używa czegoś, co nie jest bezpieczne dla wątków, samo w sobie nie jest bezpieczne dla wątków.