як дізнатися, що НЕ є потокобезпечним в рубіні?


93

починаючи з Rails 4 , за замовчуванням все повинно було працювати в різьбовому середовищі. Це означає весь код, який ми пишемо, І ВСІ коштовності, якими ми користуємось, повинні бутиthreadsafe

отже, у мене є кілька запитань щодо цього:

  1. що НЕ є потокобезпечним в ruby ​​/ rails? Vs Що безпечно для різьблення в ruby ​​/ rails?
  2. Чи існує перелік дорогоцінних каменів, який, як відомо, є безпечним або навпаки?
  3. чи існує перелік загальних шаблонів коду, які НЕ є безпечним прикладом @result ||= some_method?
  4. Чи безпечні структури даних у ядрі ruby ​​lang, такі як Hashetc threadsa?
  5. На МРТ, де є GVL/,GIL що означає, що одночасно може працювати лише 1 рубіновий потік, крім IO, чи впливає безпечна зміна потоку на нас?

2
Ви впевнені, що весь код та всі дорогоцінні камені МОЖЕ бути безпечними? У примітках до випуску сказано, що Rails сам по собі буде безпечним, а не те, що все інше, що використовується з ним,
МОЖЕ

Багатопотокові тести були б найгіршим можливим ризиком безпечності ниток. Коли вам потрібно змінити значення змінної середовища навколо вашого тестового випадку, ви миттєво не захищені від потоків. Як би ви обійшли це? І так, усі дорогоцінні камені повинні бути безпечними.
Лукас Обергубер,

Відповіді:


110

Жодна з основних структур даних не захищена від потоків. Єдине, що я знаю про те, що поставляється з Ruby, - це реалізація черги у стандартній бібліотеці ( require 'thread'; q = Queue.new).

GIL MRI не рятує нас від питань безпеки потоків. Це лише гарантує, що два потоки не можуть запускати код Ruby одночасно , тобто на двох різних процесорах одночасно. Потоки все ще можна призупинити та відновити в будь-який момент вашого коду. Якщо ви пишете код, @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }наприклад, мутацію спільної змінної з декількох потоків, значення спільної змінної згодом не є детермінованим. GIL - це більш-менш симуляція одноядерної системи, вона не змінює фундаментальних питань написання правильних паралельних програм.

Навіть якби МРТ був однопоточним, як Node.js, все одно доведеться думати про паралельність. Приклад із збільшеною змінною буде працювати нормально, але ви все одно можете отримати умови перегонів, коли все відбувається в недетермінованому порядку, а один зворотний виклик робить результат іншим. Однопотокові асинхронні системи легше міркувати, але вони не позбавлені проблем паралельності. Подумайте лише про програму з кількома користувачами: якщо два користувачі натискають редагувати в публікації переповнення стека більш-менш одночасно, витратьте трохи часу на редагування публікації, а потім натисніть «Зберегти», зміни яких побачить третій користувач пізніше, коли вони читати ту саму публікацію?

У Ruby, як і в більшості інших паралельних середовищ виконання, все, що є більш ніж однією операцією, не є безпечним для потоків. @n += 1не є потокобезпечним, оскільки це кілька операцій. @n = 1є безпечним для потоку, оскільки це одна операція (це багато операцій під капотом, і я, мабуть, зіткнувся б із проблемами, якби спробував детально описати, чому це "безпечно для потоків", але в підсумку ви не отримаєте несуперечливих результатів від завдань ).@n ||= 1, не є, і жодна інша скорочена операція + призначення також не є. Однією помилкою, яку я зробив багато разів, є написання return unless @started; @started = true, яке взагалі не є безпечним для потоків.

Я не знаю жодного авторитетного списку твердостійких та не потокових безпечних висловлювань для Ruby, але існує просте правило: якщо вираз виконує лише одну (без побічних ефектів) операцію, це, мабуть, безпечно для потоку. Наприклад: a + bце нормально, a = bце теж нормально, і a.foo(b)це нормально, якщо метод не fooмає побічних ефектів (оскільки майже все, що є в Ruby, є викликом методу, навіть призначення у багатьох випадках, це стосується і інших прикладів). Побічні ефекти в цьому контексті означають речі, які змінюють стан. неdef foo(x); @x = x; end є побічним ефектом.

Одним із найскладніших моментів написання безпечного коду потоку в Ruby є те, що всі основні структури даних, включаючи масив, хеш і рядок, можна змінювати. Дуже легко випадково просочити шматочок свого стану, і коли цей шматок змінюється, речі можуть по-справжньому зіпсуватися. Розглянемо такий код:

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

Екземпляр цього класу можна спільно використовувати між потоками, і вони можуть безпечно додавати до нього речі, але існує помилка паралельності (це не єдина): внутрішній стан об'єкта просочується через доступ stuff. Окрім того, що є проблематичним з точки зору інкапсуляції, він також відкриває банку одночасних хробаків. Можливо, хтось бере цей масив і передає його кудись ще, а цей код, у свою чергу, вважає, що зараз він володіє цим масивом і може з ним робити все, що хоче.

Ще один класичний приклад Ruby:

STANDARD_OPTIONS = {:color => 'red', :count => 10}

def find_stuff
  @some_service.load_things('stuff', STANDARD_OPTIONS)
end

find_stuffвідмінно працює при першому використанні, але вдруге повертає щось інше. Чому? load_thingsМетод буває думати , що це має хеш опцій , переданий йому, і робитьcolor = options.delete(:color) . Тепер STANDARD_OPTIONSконстанта вже не має однакового значення. Константи постійні лише в тому, на що посилаються, вони не гарантують постійність структур даних, на які вони посилаються. Подумайте лише, що сталося б, якби цей код запускався одночасно.

Якщо ви уникаєте спільного змінного стану (наприклад, змінних екземплярів в об'єктах, до яких доступно декілька потоків, структури даних, такі як хеші та масиви, доступ до яких здійснюється кількома потоками), безпека потоків не така важка. Спробуйте звести до мінімуму частини вашої програми, до яких здійснюється одночасний доступ, і зосередьте свої зусилля там. IIRC, у програмі Rails, новий об’єкт контролера створюється для кожного запиту, тому він буде використовуватися лише одним потоком, і те саме стосується будь-яких модельних об’єктів, які ви створюєте з цього контролера. Однак Rails також заохочує використовувати глобальні змінні (User.find(...) використовує глобальну зміннуUser, ви можете вважати це лише класом, і це клас, але це також простір імен для глобальних змінних), деякі з них є безпечними, оскільки вони доступні лише для читання, але іноді ви зберігаєте речі в цих глобальних змінних, оскільки це зручно. Будьте дуже обережні, коли використовуєте все, що є загальнодоступним.

Запускати Rails у різьбових середовищах можна було досить давно, тому, не будучи експертом Rails, я все одно зайшов би так далеко, сказавши, що вам не потрібно турбуватися про безпеку потоків, коли справа стосується самої Rails. Ви все ще можете створювати програми Rails, які не є безпечними для потоків, виконуючи деякі дії, про які я згадував вище. Коли справа доходить, інші дорогоцінні камені припускають, що вони не захищені від потоків, якщо вони не кажуть, що вони є, і якщо вони кажуть, що вони вважають, що це не так, і переглядають їх код (але просто тому, що ви бачите, що вони працюють так@n ||= 1 не означає, що вони не є безпечними для потоків, це цілком законне, що потрібно робити у правильному контексті - замість цього слід шукати такі речі, як змінній стан у глобальних змінних, як він обробляє змінні об’єкти, передані його методам, і особливо як це обробляє хеші опцій).

Нарешті, бути небезпечним для потоку є перехідною властивістю. Все, що використовує щось, що не є потокобезпечним, саме по собі не є безпечним для ниток.


Чудова відповідь. Враховуючи, що типовий додаток для рейок є багатопроцесорним (як ви описали, багато різних користувачів отримують доступ до одного і того ж додатка), мені цікаво, який граничний ризик потоків для моделі паралельності ... Іншими словами, наскільки більш "небезпечним" це працювати в різьбовому режимі, якщо ви вже маєте справу з деякою паралельністю за допомогою процесів?
імбирне вапно

2
@Theo Спасибі тонна. Ці постійні речі - це велика бомба. Це навіть не безпечно для процесу. Якщо константа змінюється в одному запиті, це призведе до того, що пізніші запити побачать змінену константу навіть в одному потоці. Рубінові константи дивні
сміття

5
Зробити STANDARD_OPTIONS = {...}.freezeрейз на неглибоких мутаціях
glebm

Дійсно чудова відповідь
Чейн

3
"Якщо ви пишете код як @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...], значення спільної змінної згодом не є детермінованим." - Чи знаєте ви, чи відрізняється це між версіями Ruby? Наприклад, запуск вашого коду на 1.8 дає різні значення @n, але на 1.9 і пізніше він, здається, послідовно дає @n300.
user200783

10

На додаток до відповіді Тео, я додав би кілька проблемних зон, на які слід звертати увагу саме в Rails, якщо ви переходите на config.threadsafe!

  • Змінні класу :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Нитки :

    Thread.start


9

починаючи з Rails 4, за замовчуванням все повинно було працювати в різьбовому середовищі

Це не на 100% правильно. Захисні нитки Rails увімкнені за замовчуванням. Якщо ви розгортаєтесь на багатопроцесорному сервері додатків, як Passenger (community) або Unicorn, різниці взагалі не буде. Ця зміна стосується лише вас, якщо ви використовуєте багатопотокове середовище, таке як Puma або Passenger Enterprise> 4.0

Раніше, якщо ви хотіли розгорнути на багатопотоковому сервері додатків, вам доводилося вмикати config.threadsafe , який зараз є за замовчуванням, оскільки все це не мало ефектів, або також застосовувалося до програми Rails, що працює в одному процесі ( Prooflink ).

Але якщо ви хочете отримати всі переваги потокового передавання Rails 4 та інші матеріали багатопотокового розгортання в режимі реального часу, можливо, ця стаття вам буде цікава. Як сумно @Theo, для програми Rails ви насправді просто повинні пропустити мутуючий статичний стан під час запиту. Хоча це проста практика, якої слід дотримуватися, на жаль, ви не можете бути впевнені в цьому щодо кожного дорогоцінного каменя. Наскільки я пам’ятаю, Чарльз Олівер Наттер із проекту JRuby мав кілька підказок щодо цього в цьому подкасті.

І якщо ви хочете написати суто одночасне програмування Ruby, де вам знадобляться деякі структури даних, до яких доступний більше ніж один потік, можливо, вам знадобиться самоцвіт thread_safe .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.