1. ホーム
  2. ruby

[解決済み] Rubyでスレッドセーフでないものを知るには?

2023-02-03 19:55:04

質問

Rails 4から始める になると、すべてがデフォルトでスレッド環境で実行されなければならなくなります。これが意味するのは、私たちが書くすべてのコードが すべて 私たちが使用するgemは threadsafe

で、これに関していくつか質問があります。

  1. ruby/railsでスレッドセーフでないものは何ですか? Vs ruby/railsでスレッドセーフなのは何ですか?
  2. 以下のような gems のリストはありますか? がスレッドセーフであることが知られているか、あるいはその逆であるか?
  3. スレッドセーフでないコードの一般的なパターンのリストがありますか? @result ||= some_method ?
  4. Ruby lang core のデータ構造は、次のようなものですか? Hash などのデータ構造はスレッドセーフですか?
  5. MRIでは GVL / GIL を除いて、一度に実行できるのは1つのrubyスレッドのみということになります。 IO スレッドセーフの変更は私たちに影響を与えるのでしょうか?

どのように解決するのですか?

コアとなるデータ構造のどれもがスレッドセーフではありません。私が知る限り、Ruby に同梱されているのは標準ライブラリのキュー実装だけです ( require 'thread'; q = Queue.new ).

MRIのGILは、スレッドセーフの問題から私たちを救ってはくれません。2つのスレッドがRubyのコードを実行できないようにするだけです。 を同時に実行できないようにするだけです。 つまり、2つの異なるCPUでまったく同時に実行できないようにするだけです。スレッドは、コードのどの時点でも一時停止したり再開したりすることができます。次のようなコードを書くと @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } } のようなコードを書く場合、例えば複数のスレッドから共有変数を変更する場合、その後の共有変数の値は決定論的ではありません。GIL は多かれ少なかれシングル コア システムのシミュレーションであり、正しい並行プログラムを書くという基本的な問題を変更するものではありません。

MRIがNode.jsのようにシングルスレッドだったとしても、並行処理について考えなければなりません。インクリメントされた変数の例はうまくいきますが、物事が非決定的な順序で起こり、あるコールバックが別のコールバックの結果を食い止めるようなレースコンディションが発生する可能性はまだあります。シングルスレッドの非同期システムは推論しやすいですが、並行性の問題がないわけではありません。複数のユーザーがいるアプリケーションを考えてみてください。2 人のユーザーがほぼ同時に Stack Overflow の投稿の編集をヒットし、投稿を編集して保存をヒットした場合、3 番目のユーザーが後で同じ投稿を読むときに、誰の変更が見られるでしょうか?

Rubyでは、他のほとんどの同時実行ランタイムと同様に、1つ以上の操作であるものはスレッドセーフではありません。 @n += 1 は複数の操作であるため、スレッドセーフではありません。 @n = 1 は1つの操作なのでスレッドセーフです(フードの下にはたくさんの操作があり、なぜ "スレッドセーフ" なのかを詳しく説明しようとすると面倒なことになるかもしれませんが、最終的には代入から矛盾した結果を得ることはないでしょう)。 @n ||= 1 はそうではありませんし、他の省略形操作+代入もそうではありません。私が何度も犯してきた間違いの1つは、次のように書くことです。 return unless @started; @started = true と書くことです。これはスレッドセーフではありません。

Rubyのスレッドセーフな文とスレッドセーフでない文の権威あるリストは知りませんが、簡単な経験則があります:もし式が一つの(副作用のない)操作しかしないなら、それはおそらくスレッドセーフです。例えば a + b は大丈夫です。 a = b もOK、そして a.foo(b) もOKです。 というメソッドがあれば 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 は最初に使われたときはうまく動作しますが、2回目に使われたときは別のものを返します。なぜでしょうか?それは load_things メソッドは渡されたオプションハッシュを自分のものだと思い込んでしまい color = options.delete(:color) . ここで STANDARD_OPTIONS 定数はもう同じ値を持っていません。定数は参照するものだけが一定で、参照するデータ構造の一定性を保証するものではありません。このコードが同時に実行されたらどうなるかを考えてみてください。

共有された変更可能な状態(例えば、複数のスレッドによってアクセスされるオブジェクトのインスタンス変数、複数のスレッドによってアクセスされるハッシュや配列のようなデータ構造)を避けるならば、スレッドセーフはそれほど難しいものではありません。アプリケーションの中で、同時にアクセスされる部分をなるべく少なくして、そこに力を注ぐようにしましょう。Railsアプリケーションでは、リクエストごとに新しいコントローラオブジェクトが作成されるので、単一のスレッドにしか使われません。しかし、Railsではグローバル変数( User.find(...) はグローバル変数 User のように、クラスとしか思えないかもしれませんし、クラスではありますが、グローバル変数の名前空間でもあります)、これらの中には、読み取り専用なので安全なものもありますが、便利だからと、このグローバル変数に保存してしまうこともあります。グローバルにアクセスできるものを使うときは、十分注意してください。

Railsをスレッド環境で動かすことはかなり前から可能なので、Railsの専門家でなくても、Railsそのものに関してはスレッドセーフを気にする必要はないとまで言い切れます。上に書いたようなことをすれば、スレッドセーフでないRailsアプリケーションを作ることは可能です。他のgemについては、そのgemがスレッドセーフだと言わない限りスレッドセーフではないと仮定し、もしそうだと言うならそうではないと仮定して、そのコードに目を通します (ただし、次のようなコードを書いているのを見ただけで、スレッドセーフではないと判断します)。 @n ||= 1 がスレッドセーフでないことを意味するわけではなく、正しい文脈で行う完全に正当な行為です。代わりに、グローバル変数におけるミュータブルな状態、メソッドに渡されるミュータブルなオブジェクトの処理方法、特にオプションハッシュの処理方法などを調べる必要があります)。

最後に、スレッドセーフでないことは、他律的な性質です。スレッドセーフでないものを使用するものはすべて、それ自体がスレッドセーフではありません。