2016-06-23 3 views
1

Я выполнил следующий код в консоли более 10 раз, и это привело к тому же выводу, 100000.По умолчанию Ruby Thread-Safe?

i = 0 
1000.times do 
Thread.start { 100.times { i += 1 } } 
end 
i 

Не должна ли она дать мне другого выхода, как я читаю и обновление i, используя несколько потоков. Это заставило меня задуматься, действительно ли рубин действительно поточно-безопасен по умолчанию? Если нет, то почему я всегда вижу такой же вывод?

p.s. Если вы говорите, что по умолчанию это не потокобезопасно, вы можете поделиться простым примером, который даст мне другой результат, когда я запустил консоль rails?

Edit:

Другими словами, приведенный выше код работает 1000 нитей одновременно? Если да, то результат не должен быть 100000ВСЕГДА. Если нет, то как запускать несколько потоков одновременно?

Если я добавлю puts, тогда порядок печати i изменится. Это означает, что потоки чередуются друг с другом, но работают ли они одновременно?

Я не спрашиваю, как сделать этот поток безопасным. Я понимаю концепции mutex/locking & синхронный/асинхронный процесс. Поскольку я их понимаю, я не понимаю выход этого кода.

+0

Что значит потокобезопасный, у Ruby не было бы функции «Thread», если бы было небезопасно вставлять ...? – 13aal

+0

Я имел в виду, когда я читал и обновлял локальную переменную в нескольких потоках, не должен отличаться результат? – Abhishek

+0

Интересно, как порядок применения атомной операции 'succ' к' Integer' может повлиять на результат? – mudasobwa

ответ

0

Huh !! Наконец, я нашел способ доказать, что он не приведет к 100000 всегда на irb.

Running следующий код дал мне идею,

100.times do 
i = 0 
1000.times do 
Thread.start { 100.times { i += 1 } } 
end 
puts i 
end 

Я вижу разные значения, большую часть времени. В основном, он колеблется от 91k to 100000.

3

Код не является потокобезопасным автоматически, вы должны работать, чтобы сделать его потокобезопасным.

В частности, операция += фактически представляет собой три операции: чтение, приращение, запись. Если они смешиваются с другими потоками, вы можете иметь невероятно непредсказуемое поведение.

Рассмотрим следующую последовательность событий на двух потоков:

A  B 
------------- 
READ 
     READ 
INCR 
     INCR 
WRITE 
     WRITE 

Это самый простой случай, когда вы будете иметь две операции инкремента еще, так как они оба используют один и тот же исходное значение один из них аннулировавшую.

В моих тестах это вряд ли произойдет в двухъядерной системе, но практически постоянная проблема на четырех основных машинах просто потому, что многие ведут себя как две слабосвязанные двухъядерные системы, каждая со своим собственным кешем. Это еще более заметно при использовании JRuby, где многопоточная поддержка намного лучше. Этот пример кода у вас есть Урожайность случайные ответы для меня, где-то от 98200 до 99500.

Чтобы сделать это поточно вы должны использовать Mutex или использовать атомную операцию инкремента из библиотеки как Concurrent Ruby, который даст вам инструменты чтобы сделать это безопасно.

Альтернативой является предотвращение смешивания данных между потоками или использование структуры, такой как Queue для управления связью. Ни один из двух потоков не должен манипулировать одним и тем же объектом без Mutex.

+0

Я понимаю Mutex & locking. Но я не вижу других результатов. Я на lenovo E40, intel i5, я уверен, что он может запускать более 4 потоков одновременно. – Abhishek

+0

@Abhishek i5 может планировать любое количество потоков, но только четыре будут активно выполняться в любой момент времени. Даже одноядерная машина может иметь условия горения, если планировщик запускается в неподходящее время, например, сразу после считывания. У вас нет контроля над этим *, если у вас нет Mutexes, чтобы обеспечить вам эксклюзивный доступ к определенной части данных. – tadman

+0

Точно! Итак, почему я всегда вижу такой же вывод? – Abhishek

1

«В информатике, поток исполнения является наименьшим последовательность запрограммированных инструкций, которые могут управляться независимо друг от друга с помощью операционной системы планировщика. Поток представляет собой процесс, легкий вес.»

irb(main):001:0> def calculate_sum(arr) 
irb(main):002:1> sleep(2) 
irb(main):003:1> sum = 0 
irb(main):004:1> arr.each do |item| 
irb(main):005:2*  sum += item 
irb(main):006:2> end 
irb(main):007:1> sum 
irb(main):008:1> end 
=> :calculate_sum 
irb(main):009:0> 
irb(main):010:0* @items1 = [12, 34, 55] 
=> [12, 34, 55] 
irb(main):011:0> @items2 = [45, 90, 2] 
=> [45, 90, 2] 
irb(main):012:0> @items3 = [99, 22, 31] 
=> [99, 22, 31] 
irb(main):013:0> 
irb(main):014:0* threads = (1..3).map do |i| 
irb(main):015:1* Thread.new(i) do |i| 
irb(main):016:2*  items = instance_variable_get("@items#{i}") 
irb(main):017:2>  puts "items#{i} = #{calculate_sum(items)}" 
irb(main):018:2> end 
irb(main):019:1> end 
=> [#<Thread:[email protected](irb):15 run>, #<Thread:[email protected](irb):15 run>, #<Thread:[email protected](irb):15 run>] 
irb(main):020:0> threads.each {|t| t.join} 
items3 = 152 
items2 = 137 
items1 = 101 
=> [#<Thread:[email protected](irb):15 dead>, #<Thread:[email protected](irb):15 dead>, #<Thread:[email protected](irb):15 dead>] 
irb(main):021:0> 

Это базовый пример потоковой обработки процесса в Ruby. У вас есть основной метод calculate_sum, который принимает массив в качестве аргумента @item1, @item2, @item3. Оттуда вы делаете три потока threads = (1..3), сопоставляете их в свою собственную переменную .map do |i| и запускаете новый экземпляр Thread с переменной, на которую был нарисован нить, Thread.start(i).

Отсюда вы создаете переменную элемента, которая равна переменной экземпляра items = instance_variable_get(<object>), выводя результат вычисления, .

Как видите, потоки начинают работать одновременно => [#<Thread:[email protected](irb):15 run>, #<Thread:[email protected](irb):15 run>, #<Thread:[email protected](irb):15 run>]. Все потоки выполняются путем вызова каждого потока и объединения их threads.each {|t| t,join}.

Последний раздел является самым важным, все потоки выполняются и умирают одновременно, если поток имеет очень длительный процесс, который поток должен заканчиваться до завершения программы. Пример:

irb(main):023:0> Thread.new do 
irb(main):024:1* puts t 
irb(main):025:1> Thread.new do 
irb(main):026:2*  sleep(5) 
irb(main):027:2>  puts h 
irb(main):028:2> end 
irb(main):029:1> end 
=> #<Thread:[email protected](irb):23 run> 
irb(main):030:0> hello 
goodbye 

Вторая нить никогда не выходит, поэтому она будет продолжать работать, пока вы не сократите выполнение.

В главном примере конец имеет => [#<Thread:[email protected](irb):15 dead>, #<Thread:[email protected](irb):15 dead>, #<Thread:[email protected](irb):15 dead>], потому что все потоки завершают процесс и немедленно выходят. Для завершения моего процесса вам нужно будет предоставить exit для второго потока.

Надеюсь, это ответит на ваши вопросы.

0

К сожалению, Ruby не имеет официально указанной модели памяти, такой как Java, поскольку Java 5 или C++ имеет с C++ 11.

Фактически, Ruby действительно не имеет официальной спецификации вообще, хотя в нем было несколько попыток, все они имеют ту же проблему, что дизайнеры Ruby на самом деле их не используют. Итак, единственная спецификация, которую Ruby имеет, в основном «независимо от того, что делает YARV». (И, например, ISO Ruby Language Specification просто не указывает класс Thread, тем самым полностью устраняя проблему.)

НО НО !!! Для параллелизма это в основном непригодно, потому что YARV не может запускать потоки параллельно, поэтому в YARV просто не возникает проблем с параллелизмом, поэтому основная библиотека не защищает эти проблемы! Однако, если бы мы сказали, что семантика параллелизма Ruby - это то, что делает YARV, теперь возникает вопрос: является ли тот факт, что мы не можем иметь параллелизм в семантике? Является ли тот факт, что основные библиотеки не защищены частью семантики?

Это борьба, с которой сталкиваются реализации, такие как JRuby, Rubinius, IronRuby, MacRuby и т. Д., Которые имеют потоки, которые могут работать параллельно. И они все еще работают над выяснением ответов.

Итак, tl; dr ответ на ваш вопрос: мы не знаем, является ли Ruby потокобезопасным, потому что мы не знаем, что такое семантика Ruby.

Это довольно распространено для многопоточных программ, которые отлично работают на YARV, чтобы разбить JRuby, например, но опять же, это ошибка программы или JRuby? Мы не можем сказать, потому что у нас нет спецификации, которая сообщает нам, что должно быть многопоточным поведением реализации Ruby. Мы могли бы сделать простой выход и сказать, что Ruby - это то, что YARV делает, и когда программа работает на YARV, тогда мы должны изменить JRuby, чтобы программа также работала над YARV. Однако параллелизм на самом деле является одной из основных причин, почему люди выбирают JRuby в первую очередь, поэтому это просто невозможно.

Смежные вопросы