Rubyでスレッドセーフでないものを知る方法は?


92

Rails 4以降、デフォルトではすべてがスレッド環境で実行される必要があります。これが意味することは、私たちが書くコードのすべてである ALL我々が使用する宝石があることが要求されていますthreadsafe

だから、私はこれについていくつか質問があります:

  1. ruby / railsでスレッドセーフでないものは何ですか?ルビー/レールのスレッドセーフは何ですか?
  2. スレッドセーフまたはその逆であること知られている宝石のリストはありますか?
  3. スレッドセーフな例ではないコードの一般的なパターンのリストはあり@result ||= some_methodますか?
  4. Hashetcなどのruby langコアのデータ構造はスレッドセーフですか?
  5. MRIでは、GVL/GILを除いて一度に実行できるルビスレッドが1つだけであることを意味する/IOがありますが、スレッドセーフな変更は影響を及ぼしますか?

2
すべてのコードとすべてのgemがスレッドセーフになることを確信していますか?どのようなリリースノートの言うことはRailsの自身がそれを使用する他のすべてがでなければならないことを、スレッドセーフになるということです
enthrops

マルチスレッドテストは、スレッドセーフのリスクとしては最悪の場合です。テストケースの周囲の環境変数の値を変更する必要がある場合、即座にスレッドセーフではなくなります。それをどのように回避しますか?そして、はい、すべての宝石はスレッドセーフでなければなりません。
Lukas Oberhuber 2013

回答:


109

コアデータ構造はスレッドセーフではありません。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のようにシングルスレッドであったとしても、並行性について考える必要があります。インクリメントされた変数を使用した例は問題なく機能しますが、非決定的な順序で発生し、1つのコールバックが別のコールバックの結果を上書きするという競合状態を取得できます。シングルスレッドの非同期システムの方が理由はわかりやすいですが、同時実行の問題から解放されているわけではありません。複数のユーザーがいるアプリケーションを考えてみてください。2人のユーザーがほぼ同じ時間にStack Overflow投稿の編集をヒットした場合、投稿の編集にしばらく時間を費やしてから保存をクリックします。その変更は、後で3人目のユーザーに表示されます同じ投稿を読みますか?

Rubyでは、他のほとんどの並行ランタイムと同様に、複数の操作を行うとスレッドセーフではありません。@n += 1複数の操作であるため、スレッドセーフではありません。@n = 1これは1つの操作であるため、スレッドセーフです(フード内では多くの操作が行われます。「スレッドセーフ」である理由を詳細に説明しようとすると、おそらく問題が発生しますが、割り当てから一貫性のない結果が得られることはありません。 )。@n ||= 1、ではなく、他の省略操作+割り当てもありません。私が何度も犯した1つの間違いはを書くことですがreturn unless @started; @started = true、これはスレッドセーフではありません。

Rubyのスレッドセーフステートメントと非スレッドセーフステートメントの正式なリストは知りませんが、簡単な経験則があります。式が1つの(副作用のない)操作のみを実行する場合は、おそらくスレッドセーフです。例:a + bis ok、a = bis ok、and a.foo(b)ok、if the method foois side-effect free(なぜなら、Rubyのほとんどすべてがメソッド呼び出しであり、多くの場合、割り当てであっても、これは他の例にも当てはまります)このコンテキストでの副作用は、状態を変更するものを意味します。副作用def foo(x); @x = x; endはありません

Rubyでスレッドセーフコードを記述する場合の最も難しい点の1つは、配列、ハッシュ、文字列を含むすべてのコアデータ構造が変更可能であることです。誤って状態の一部をリークすることは非常に簡単であり、その一部が変更可能である場合、物事は本当に台無しになる可能性があります。次のコードを検討してください。

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アクセサーを通じてリークします。カプセル化の観点から問題があるだけでなく、同時実行ワームの可能性も開きます。たぶん誰かがその配列を受け取って別の場所に渡し、そのコードは今度はそれがその配列を所有していると考え、それを使って何でもできると思います。

もう1つの古典的な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定数は同じ値ではなくなりました。定数は、参照する内容が一定であり、参照するデータ構造の不変性を保証するものではありません。このコードを同時に実行するとどうなるかを考えてください。

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

かなり長い間、スレッド環境でRailsを実行することが可能でした。そのため、Railsの専門家でなくても、Rails自体に関してはスレッドの安全性について心配する必要はないと言っています。上記のいくつかのことを行うことで、スレッドセーフではないRailsアプリケーションを作成できます。それが来るとき、他の宝石は彼らがそうであると言わない限り彼らがスレッドセーフではないと仮定し、彼らがそうではないと仮定していると彼らが言った場合、彼らのコードを調べます(しかし、彼らが@n ||= 1 スレッドセーフではないという意味ではありません。これは、適切なコンテキストで実行することは完全に正当なことです。代わりに、グローバル変数で変更可能な状態、メソッドに渡された変更可能なオブジェクトを処理する方法、特にその方法を調べる必要があります。オプションのハッシュを処理します)。

最後に、スレッドが安全でないことは推移的な特性です。スレッドセーフでないものを使用するものは、それ自体がスレッドセーフではありません。


すばらしい答えです。典型的なRailsアプリがマルチプロセス(あなたが説明したように、同じアプリにアクセスする多くの異なるユーザー)であることを考えると、同時実行モデルに対するスレッドの限界リスクは何なのかと疑問に思っています...つまり、どれほど「危険」なのでしょうか。プロセスを介していくつかの同時実行性をすでに処理している場合、それはスレッドモードで実行されますか?
ジンジャーライム2013

2
@Theoトンありがとう。その絶え間ないものは大きな爆弾です。それはプロセス安全でもありません。定数が1つの要求で変更された場合、それ以降の要求では、単一のスレッドでも変更された定数が表示されます。Rubyの定数は奇妙です
2013

5
やるSTANDARD_OPTIONS = {...}.freeze浅い変異に調達する
glebm

本当に素晴らしい答え
チェイン2017年

3
@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...]のようなコードを記述した場合、その後の共有変数の値は確定的ではありません。」-これがRubyのバージョン間で異なるかどうか知っていますか?例えば、1.8上でコードを実行するとの異なる値を与え@nたが、後に1.9と上では一貫して与えるようで@n300に等しい
user200783

10

Theoの回答に加えて、config.threadsafeに切り替える場合は、Railsで特に注意すべき問題の領域をいくつか追加します。

  • クラス変数

    @@i_exist_across_threads

  • ENV

    ENV['DONT_CHANGE_ME']

  • スレッド

    Thread.start


9

Rails 4以降、デフォルトではすべてがスレッド環境で実行される必要があります

これは100%正しくありません。スレッドセーフなRailsはデフォルトでオンになっています。Passenger(コミュニティ)やUnicornなどのマルチプロセスアプリサーバーにデプロイする場合、違いはありません。この変更は、PumaまたはPassenger Enterprise> 4.0などのマルチスレッド環境にデプロイする場合にのみ関係します。

以前は、マルチスレッドのアプリケーションサーバーにデプロイする場合、config.threadsafeをオンにする必要がありました。これは、デフォルトでは無効になっているか、単一のプロセスで実行されているRailsアプリにも適用されていたためです(Prooflink)。

しかし、Rails 4 ストリーミングのメリットや、マルチスレッドデプロイメントのその他のリアルタイム機能がすべて必要な場合は、この記事が興味深いでしょう。@Theoが悲しいように、Railsアプリの場合、実際には、リクエスト中に静的な状態を変更することを省略しなければなりません。これは従うべき簡単な方法ですが、残念ながら、見つけたすべての宝石についてこれについて確信が持てません。私が覚えている限り、JRubyプロジェクトのCharles Oliver Nutterがこのポッドキャストでそれについていくつかのヒントを持っていました。

そして、もしあなたが純粋な並行Rubyプログラミングを書きたければ、複数のスレッドによってアクセスされるいくつかのデータ構造が必要になるでしょう、おそらくthread_safe gemが役立つでしょう。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.