Жодна з основних структур даних не захищена від потоків. Єдине, що я знаю про те, що поставляється з 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
не означає, що вони не є безпечними для потоків, це цілком законне, що потрібно робити у правильному контексті - замість цього слід шукати такі речі, як змінній стан у глобальних змінних, як він обробляє змінні об’єкти, передані його методам, і особливо як це обробляє хеші опцій).
Нарешті, бути небезпечним для потоку є перехідною властивістю. Все, що використовує щось, що не є потокобезпечним, саме по собі не є безпечним для ниток.