począwszy od Rails 4 , wszystko musiało domyślnie działać w środowisku wątkowym. Oznacza to, że cały kod, który piszemy ORAZ wszystkie klejnoty, których używamy, muszą byćthreadsafe
więc mam kilka pytań na ten temat:
- co NIE jest bezpieczne dla wątków w Ruby / Rails? Vs Co jest bezpieczne dla gwintów w Ruby / Rails?
- Czy istnieje lista klejnotów, o których wiadomo, że są bezpieczne dla wątków lub odwrotnie?
- czy istnieje lista typowych wzorców kodu, które NIE są przykładem ochrony wątków
@result ||= some_method
? - Czy struktury danych w rdzeniu języka Ruby, takie jak
Hash
itp., Są bezpieczne dla wątków? - Na MRI, gdzie występuje
GVL
/,GIL
co oznacza, że tylko 1 nitka rubinowa może działać na raz, z wyjątkiemIO
, czy zmiana ochrony wątków wpływa na nas?
ruby
multithreading
concurrency
thread-safety
ruby-on-rails-4
CuriousMind
źródło
źródło
Odpowiedzi:
Ż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 pisaniereturn 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 ia.foo(b)
jest w porządku, jeśli metodafoo
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 robicolor = 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.
źródło
STANDARD_OPTIONS = {...}.freeze
wychowywać na płytkich mutacjach@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
[...], wartość wspólnej zmiennej później nie jest deterministyczna”. - Czy wiesz, czy różni się to między wersjami Rubiego? Na przykład uruchomienie kodu na 1.8 daje różne wartości@n
, ale w 1.9 i późniejszych wydaje się, że konsekwentnie daje@n
równe 300.Oprócz odpowiedzi Theo, dodałbym kilka problemów, na które należy zwrócić uwagę w Railsach, jeśli przełączasz się na config.threadsafe!
Zmienne klasowe :
@@i_exist_across_threads
ENV :
ENV['DONT_CHANGE_ME']
Wątki :
Thread.start
źródło
To nie jest w 100% poprawne. Railsy bezpieczne dla wątków są domyślnie włączone. W przypadku wdrożenia na serwerze aplikacji obsługującym wiele procesów, takim jak Passenger (społeczność) lub Unicorn, nie będzie żadnej różnicy. Ta zmiana dotyczy tylko Ciebie, jeśli wdrażasz w środowisku wielowątkowym, takim jak Puma lub Passenger Enterprise> 4.0
W przeszłości, jeśli chciałeś wdrożyć na wielowątkowym serwerze aplikacji, musiałeś włączyć config.threadsafe , co jest teraz domyślne, ponieważ wszystko, co robił, albo nie miało żadnego wpływu, albo było również stosowane do aplikacji Rails działającej w jednym procesie ( Prooflink ).
Ale jeśli chcesz korzystać ze wszystkich zalet przesyłania strumieniowego Rails 4 i innych rzeczy związanych z wdrażaniem wielowątkowym w czasie rzeczywistym, być może ten artykuł okaże się interesujący. Jak @Theo smutny, w przypadku aplikacji Railsów musisz po prostu pominąć mutowanie statycznego stanu podczas żądania. Chociaż jest to prosta praktyka do naśladowania, niestety nie możesz być tego pewien dla każdego znalezionego klejnotu. O ile pamiętam, Charles Oliver Nutter z projektu JRuby udzielił kilku wskazówek na ten temat w tym podcastu.
A jeśli chcesz napisać czyste, współbieżne programowanie w Rubim, gdzie będziesz potrzebować pewnych struktur danych, do których dostęp ma więcej niż jeden wątek, być może okaże się przydatny gem thread_safe .
źródło