81

starting from Rails 4, wszystko musiałoby działać domyślnie w środowisku z wątkami. Oznacza to cały kod piszemy IWSZYSTKIE klejnotów używamy muszą być threadsafejak się dowiedzieć, co NIE jest bezpieczne w wątku w ruby?

tak, mam kilka pytań na ten temat:

  1. co nie jest bezpieczny wątku w rubinach/szynach? Vs Co to jest wątek bezpieczny w ruby ​​/ szyny?
  2. Czy istnieje lista klejnotów, które to znane jako threadafe lub vice-versa?
  3. jest tam Lista typowych wzorców kodu, które NIE są przykładami Threadafe @result ||= some_method?
  4. Czy struktury danych w rdzeniu języka rubinowego są takie, jak Hash itd. Threadafe?
  5. W przypadku MRI, gdzie istnieje GVL/GIL, co oznacza, że ​​tylko 1 wątek ruby ​​może działać w tym samym czasie, z wyjątkiem IO, czy zmiana bezpieczeństwa wątków wpływa na nas?
+2

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

+0

. 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. –

Odpowiedz

92

Ż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.

+0

Świetna odpowiedź. Biorąc pod uwagę, że typowa aplikacja szyny jest wieloprocesowa (jak opisałeś, wielu różnych użytkowników uzyskujących dostęp do tej samej aplikacji), zastanawiam się, co to jest * marginalne ryzyko * wątków dla modelu współbieżności ... Innymi słowy, o ile więcej " niebezpieczne "czy ma działać w trybie wątkowym, jeśli masz już do czynienia z pewną współbieżnością za pośrednictwem procesów? – gingerlime

+1

@Theo Dzięki za tonę. Te stałe rzeczy to wielka bomba. Nie jest nawet bezpieczny proces.Jeśli stała zostanie zmieniona w jednym żądaniu, spowoduje to, że późniejsze żądania zobaczą zmienioną stałą nawet w jednym wątku. Stałe Ruby są dziwne – rubish

+3

Wykonaj 'STANDARD_OPTIONS = {...}. Freeze', aby podnieść na płytkich mutacjach – glebm

9

Oprócz odpowiedzi Theo, dodałem kilka obszarów problemowych do sprawdzenia w Railsach, jeśli przejdziesz na config.threadsafe!

  • zmienne klasy:

    @@i_exist_across_threads

  • ENV:

    ENV['DONT_CHANGE_ME']

  • wątki:

    Thread.start

7

począwszy od Rails 4, wszystko miałoby działać w środowisku gwintowanych domyślnie

To nie jest 100% poprawne. Railsy Threadsafe są domyślnie włączone. Jeśli nadal będziesz wdrażać na serwerze wieloprocesorowym aplikacji, takim jak pasażer (społeczność) lub jednorożec, nie będzie różnicy. Ta zmiana dotyczy tylko ciebie, jeśli wdrożyć w środowisku wielowątkowym jak Puma czy osobowych Przedsiębiorstwo> 4,0

W przeszłości, jeśli chce wdrożyć na wielowątkowych aplikacji serwera trzeba było włączyć config. Threadafe, który jest teraz domyślny, ponieważ wszystko, co zrobił, nie miało żadnych efektów lub zostało zastosowane do aplikacji szyny uruchomionej w jednym procesie (Prooflink).

Ale jeśli chcesz wszystkie szyny 4 streaming korzyści i inne rzeczy w czasie rzeczywistym z wielowątkowym rozmieszczenia potem może znajdziesz this artykuł ciekawy. Jako @Theo smutny, dla aplikacji rails, musisz po prostu pominąć mutowanie stanu statycznego 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 miał pewne wskazówki na temat tego podcastu w this.

A jeśli chcesz napisać czysty programowanie współbieżne rubinowy, gdzie trzeba trochę struktur danych, które są dostępne przez więcej niż jednego wątku być może znajdzie Przydatne thread_safe GEM

Powiązane problemy