44457


以下のrubyのコードがrubyのバージョンによって挙動が違います。  
これについて、公式のドキュメントはありますでしょうか?

また、組み込みメソッドがスレッドセーフかどうかは何を参照すれば良いでしょうか?

[code]

    THREAD_NUM = 3
    
    Array.new(THREAD_NUM) do
      Thread.new do
        10.times do
          puts "hoge"
        end
      end
    end.each(&:join)
    
    puts "-"*5
    
    Array.new(THREAD_NUM) do
      Thread.new do
        10.times do
          print "hoge\n"
        end
      end
    end.each(&:join)
    
[/code]

* * *
[code]

    $ ruby -v
    ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin16]
    $ruby thread.rb
    hoge
    hoge
    hoge
    hoge
    hogehoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    -----
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    
[/code]

* * *
[code]

    $ ruby -v
    ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin16]
    $ ruby thread.rb
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    -----
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    hoge
    
[/code]


CC BY-SA 4.0
2018-05-27T05:10:58.293
44288
2018-06-02T07:00:02.450
28630
question
ruby
rubyの組み込みメソッドがスレッドセーフか否かについて


この回答は公式のCでの実装であるCRubyについてです。JRuby等でも同じとは限らない事に注意してください。

現在の所、CRubyの実装上のほとんどのメソッドはスレッドセーフに **見えます**
。しかし、`Thread`や`Mutex`等のスレッド関係を除き、スレッドセーフであると期待してはいけません。 **スレッドセーフではない**
という前提でコードを実装すべきです。

現在のCRubyのスレッドに関する戦略は、個々のメソッドのCレベルでの実装でスレッドセーフであるかは考慮しないとしています。これは、

  * スレッドセーフのコードは複雑になり、バグ無く維持していくのが難しい
  * シングルスレッドでは余計なロック処理によって速度低下が発生する
  * Cライブラリのほんとんどがスレッドセーフではない

などが理由です。かといって、これをそのままマルチスレッドにした場合、segmentation
faultで落ちます。そのため、Rubyではグローバルインタープリンタロック(Global Interpreter Lock, 以下GIL,
RubyではグローバルVMロックとも言われる)という仕組みを採用しています(CPythonも採用しています)。これは、マルチスレッドであっても(Cレベルでは)動いているコードは常に一つにするという制限をかけるものです。動いているスレッドは常に一つであるため、スレッドセーフではないコードであっても、その処理自体はスレッドセーフのように動作が保証できるというものです。これが冒頭で書いた「スレッドセーフに見えます」と言っているところです。

ただ、これはCレベルでの処理においてオブジェクトを保護するための機能に過ぎません。ブロックや他のメソッド呼び出しなどで一旦Rubyのコードに移ってしまえば、そこでスレッドの切り替えの発生があり得ます。また、Cレベルでスレッドセーフな部分があれば、GILを外してもいいため、その時は同時に別スレッドが動作している場合もあります。つまり、そのような場合に、そのメソッドの処理の途中で他のスレッドに割り込みされる可能性があるため、アトミックな操作であるという保証はなくなってしまうということです。

参考:  
[マルチスレッド/プロセスまとめ（Ruby編）](https://qiita.com/masashi127/items/b186bbf20b4c9632cc86)  
[Rubyのスレッド周りの話](https://qiita.com/motsat/items/8c9b6bc56152444f50a0)  
[Rubyでスレッドセーフでないことを簡単に確認したい -
もょもとの技術ノート](http://moyomot.hatenablog.com/entry/2014/05/04/232538)

* * *

今回のコードの`puts`がどうなっているかという話をしましょう。`puts`は最終的に`$stdout`に対して`IO#write`を呼び出すという形なっています。`IO#write`を上書きしたら、`puts`の動作も変わります。次のコードを見てください。

[code]

    class IO
        alias :write_org :write
        def write(str)
            write_org("[+++]")
            write_org(str)
            write_org("[---]")
        end
    end
    
    puts "hoge"
    
[/code]

実行すると

[code]

    [+++]hoge[---][+++]
    [---]
    
[/code]

となります。つまり、`IO#write`を二回呼び出しています。この呼び出しは一旦Rubyの処理に戻りますので、この瞬間スレッドの切り替えが発生する可能性があります。もし、このタイミングで切り替えが発生した場合は、別の物に割り込まれる形になるでしょう。

また、`IO#write`のような処理では内部でのIO書き込み時にGILは外すように実装されています。IO処理は重いため、その間別のスレッドが走るようにするためです。そうなると、この外れている僅かな時間でもう一つのスレッドが走り、割り込むような形で入る可能性があります。

スレッド切り替えはあくまで可能性であって、バージョン以外にも環境によっても左右されます。起きる場合もあれば全然起きない場合もあります。文字列をもっと長くすれば、発生する可能性が増えることでしょう。

※
スレッド切り替えはIO処理の他に時間経過による切り替えがありますが、それが発生するのはRubyレベルでの処理があった時になります。Cレベルでの処理中は切り替えは発生しません。

参考:  
[第19章 スレッド](http://i.loveruby.net/ja/rhg/book/thread.html)

その他にスレッドセーフではない例を一つ書いておきます。

[code]

    class String
      alias :plus :+
      def +(other)
        Thread.pass
        self.plus(other)
      end
    end
    
    arr0 = ["0"] * 10
    arr1 = ["1"] * 10
    arr = []
    arr.replace(arr0)
    ths = []
    
    ths << Thread.new do
      10.times do
        arr.replace(arr0)
        Thread.pass
      end
    end
    
    ths << Thread.new do
      10.times do
        arr.replace(arr1)
        Thread.pass
      end
    end
    
    ths << Thread.new do
      10.times do
        puts arr.sum("")
        Thread.pass
      end
    end
    
    ths.each(&:join)
    
[/code]

もし、`Array#sum`がスレッドセーフであれば、"0000000000"か"1111111111"が出力されますが、実際は0と1が混じった文字列が出力される場合があります。


CC BY-SA 4.0
2018-06-01T12:57:59.020
44457
2018-06-01T12:57:59.020
7347
44288
answer