Żadna z podstawowych struktur danych nie jest bezpieczna dla wątków. Jedyną znaną mi z Ruby jest implementacja kolejki w standardowej bibliotece (require 'thread'; q = Queue.new
).
MRI's GIL nie ratuje nas od kwestii bezpieczeństwa wątku. Upewnia się tylko, że dwa wątki nie mogą uruchomić kodu Ruby w tym samym czasie, tj. Na dwóch różnych procesorach w tym samym czasie. Wątki mogą być nadal wstrzymywane i wznawiane w dowolnym momencie kodu. Jeśli piszesz kod taki jak @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
, np. mutowanie zmiennej współdzielonej z wielu wątków, wartość późniejszej zmiennej dzielonej nie jest deterministyczna. GIL jest mniej więcej symulacją pojedynczego systemu, nie zmienia podstawowych problemów związanych z pisaniem poprawnych programów współbieżnych.
Nawet jeśli MRI był jednowątkowy jak Node.js, nadal musiałbyś myśleć o współbieżności. Przykład z inkrementowaną zmienną działałby dobrze, ale nadal można uzyskać warunki wyścigu, w których rzeczy zdarzają się w niedeterministycznej kolejności, a jedno wywołanie zwrotne blokuje wynik innego. Jednowątkowe systemy asynchroniczne są łatwiejsze do zrozumienia, ale nie są wolne od problemów z współbieżnością. Wystarczy pomyśleć o aplikacji z wieloma użytkownikami: jeśli dwóch użytkowników rozpocznie edycję na stosie przepełnienia stosu mniej więcej w tym samym czasie, poświęć trochę czasu na edycję postu, a następnie naciśnij przycisk Zapisz, którego zmiany będą widoczne dla trzeciego użytkownika później, gdy przeczytać ten sam wpis?
W języku Ruby, 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 dla wątków, ponieważ jest to wiele operacji. @n = 1
jest bezpieczny dla wątków, ponieważ jest to jedna operacja (to wiele operacji pod maską i prawdopodobnie wpadnę w kłopoty, jeśli spróbuję opisać, dlaczego jest ona "bezpieczna dla wątków", ale ostatecznie nie uzyskasz niespójnych wyników z zadania). @n ||= 1
, nie jest i żadna inna skrócona operacja + przypisanie jest albo. Jednym błędem, który popełniłem wiele razy, jest pisanie return unless @started; @started = true
, które w ogóle nie jest bezpieczne.
Nie znam żadnej wiarygodnej listy bezpiecznych i niewymagających wątku instrukcji dla Rubiego, ale istnieje prosta zasada: jeśli wyrażenie wykonuje tylko jedną operację (bez efektów ubocznych), prawdopodobnie bezpieczny wątek.Na przykład: a + b
jest ok, a = b
jest również ok, a a.foo(b)
jest ok, , jeśli metoda foo
jest efektem ubocznym darmo (ponieważ prawie wszystko w Ruby jest wywołanie metody, nawet przypisania w wielu przypadkach, to idzie do inne przykłady). Skutki uboczne w tym kontekście oznaczają rzeczy, które zmieniają stan. def foo(x); @x = x; end
jest nie efekt uboczny darmo.
Jedną z najtrudniejszych rzeczy na temat pisania bezpiecznego kodu wątku w Ruby jest to, że wszystkie podstawowe struktury danych, w tym tablica, hasz i ciąg, są zmienne. Bardzo łatwo przypadkowo przeciekać kawałek twojego stanu, a kiedy ten element jest zmienny, rzeczy mogą się bardzo zmartwić. Rozważmy 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 mogą być dzielone pomiędzy wątkami i mogą bezpiecznie dodać rzeczy do niego, ale nie jest to bug współbieżności (to nie jedyny): stan wewnętrzny obiektu przecieka przez akcesor stuff
. Poza tym, że jest problematyczna z perspektywy enkapsulacji, otwiera również puszkę robaków współbieżnych. Może ktoś bierze tę tablicę i przekazuje ją gdzie indziej, a ten kod z kolei uważa, że teraz jest właścicielem tej tablicy i może robić, co chce.
Innym klasycznym przykładem Ruby jest taka:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
działa poprawnie za pierwszym razem jest używany, ale zwraca coś innego po raz drugi. Czemu? Metoda load_things
zdaje się uważać, że jest właścicielem wartości mieszania opcji przekazanej do niej i ma wartość color = options.delete(:color)
. Stała STANDARD_OPTIONS
nie ma już tej samej wartości. Stałe są stałe w tym, do czego się odnoszą, nie gwarantują stałości struktur danych, do których się odnoszą. Pomyśl, co by się stało, gdyby ten kod był uruchamiany jednocześnie.
Jeśli unikniesz współdzielonego stanu zmiennego (na przykład zmiennych instancji w obiektach, do których dostęp ma wiele wątków, struktur danych, takich jak hashe i tablice dostępne dla wielu wątków), bezpieczeństwo wątków nie jest tak trudne. Postaraj się zminimalizować części aplikacji, które są dostępne jednocześnie, i skup się na nich. IIRC, w aplikacji Rails, dla każdego żądania tworzony jest nowy obiekt kontrolera, więc będzie używany tylko przez jeden wątek, a to samo dotyczy wszystkich obiektów modelu tworzonych przez ten kontroler. Jednak Rails zachęca również do używania zmiennych globalnych (User.find(...)
używa zmiennej globalnej User
, możesz myśleć o niej jako 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ą one tylko do odczytu, ale czasami zapisujesz rzeczy w tych zmiennych globalnych, ponieważ jest to wygodne. Zachowaj ostrożność, używając wszystkiego, co jest globalnie dostępne.
Od dłuższego czasu możliwe jest uruchamianie Railsów w środowiskach z gwintami, więc bez bycia ekspertem od Railsa, posunęłbym się do stwierdzenia, że nie musisz się martwić o bezpieczeństwo wątków w Railsach samo. Nadal możesz tworzyć aplikacje Railsowe, które nie są bezpieczne dla wątków, wykonując niektóre z wymienionych powyżej rzeczy. Kiedy przychodzi inne klejnoty, zakładają, że nie są bezpieczne dla wątków, chyba że mówią, że są, i jeśli mówią, że są założone, że nie są, i przeglądają swój kod (ale tylko dlatego, że widzisz, że robią takie rzeczy jak @n ||= 1
nie oznacza to, że nie są bezpieczne dla wątków, jest to całkowicie uzasadniona rzecz w odpowiednim kontekście - powinieneś zamiast tego szukać takich rzeczy, jak stan zmienny w zmiennych globalnych, jak radzi sobie z obiektami zmiennymi przekazanymi do ich metod, a zwłaszcza jak radzi sobie z nimi hashy opcji).
Wreszcie, bycie wątkiem niebezpiecznym jest własnoś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.
Czy jesteś pewien, że cały kod i wszystko klejnoty MUSZĄ być bezpieczne dla wątków? W informacjach o wersji mówi się, że same Railsy będą bezpieczne dla wątków, a nie wszystko inne, co jest z nim związane, musi być – enthrops
. Testy wielowątkowe byłyby najgorszym możliwym zagrożeniem dla wątków. Kiedy musisz zmienić wartość zmiennej środowiskowej wokół twojego przypadku testowego, natychmiast nie jesteś bezpieczny dla wątków. Jak byś to obejrzał? I tak, wszystkie klejnoty muszą być bezpieczne dla wątków. –