skąd wiedzieć, co NIE jest bezpieczne dla wątków w rubinie?

93

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:

  1. co NIE jest bezpieczne dla wątków w Ruby / Rails? Vs Co jest bezpieczne dla gwintów w Ruby / Rails?
  2. Czy istnieje lista klejnotów, o których wiadomo, że są bezpieczne dla wątków lub odwrotnie?
  3. czy istnieje lista typowych wzorców kodu, które NIE są przykładem ochrony wątków @result ||= some_method?
  4. Czy struktury danych w rdzeniu języka Ruby, takie jak Hashitp., Są bezpieczne dla wątków?
  5. Na MRI, gdzie występuje GVL/,GIL co oznacza, że ​​tylko 1 nitka rubinowa może działać na raz, z wyjątkiem IO, czy zmiana ochrony wątków wpływa na nas?
CuriousMind
źródło
2
Czy na pewno cały kod i wszystkie klejnoty MUSZĄ być bezpieczne dla wątków? Uwagi do wydania mówią, że same Railsy będą bezpieczne dla wątków, a nie wszystko inne używane z nim
MUSI BYĆ
Testy wielowątkowe byłyby największym możliwym zagrożeniem dla wątków. Kiedy musisz zmienić wartość zmiennej środowiskowej wokół twojego przypadku testowego, natychmiast nie jesteś bezpieczny wątkowo. Jak byś to obejść? I tak, wszystkie klejnoty muszą być zabezpieczone wątkami.
Lukas Oberhuber

Odpowiedzi:

110

Ż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 += 1nie jest bezpieczny wątkowo, ponieważ jest to wiele operacji. @n = 1jest 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 + bjest w porządku, a = bjest również w porządku i a.foo(b)jest w porządku, jeśli metoda foojest 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 stuffakcesor. 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_stuffdziała dobrze przy pierwszym użyciu, ale zwraca coś innego za drugim razem. Czemu? load_thingsMetoda 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.

Theo
źródło
Świetna odpowiedź. Biorąc pod uwagę, że typowa aplikacja railsowa jest wieloprocesowa (jak opisałeś, wielu różnych użytkowników uzyskuje dostęp do tej samej aplikacji), zastanawiam się, jakie jest marginalne ryzyko wątków w modelu współbieżności ... Innymi słowy, o ile bardziej „niebezpieczne” czy ma działać w trybie wątkowym, jeśli już masz do czynienia z pewną współbieżnością za pośrednictwem procesów?
gingerlime
2
@Theo Thanks a ton. Ta stała rzecz to wielka bomba. Nie jest to nawet bezpieczne dla procesu. 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
5
Czy STANDARD_OPTIONS = {...}.freezewychowywać na płytkich mutacjach
glebm
Naprawdę świetna odpowiedź
Cheyne
3
„Jeśli piszesz kod taki jak @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 @nrówne 300.
user200783
10

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

crizCraig
źródło
9

począwszy od Rails 4, wszystko musiało domyślnie działać w środowisku wątkowym

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 .

dre-hh
źródło