コア データ構造はどれもスレッド セーフではありません。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 の投稿で編集を押した場合、投稿の編集にしばらく時間を費やしてから保存をクリックします。同じ投稿を読みますか?
Ruby では、他のほとんどの同時実行ランタイムと同様に、複数の操作を行うものはすべてスレッド セーフではありません。@n += 1
複数の操作であるため、スレッドセーフではありません。@n = 1
1 つの操作であるため、スレッド セーフです (内部では多くの操作が行われます。「スレッド セーフ」である理由を詳細に説明しようとすると、おそらく問題が発生するでしょうが、最終的には、代入から一貫性のない結果が得られることはありません)。 )。@n ||= 1
、ではなく、他の省略形の操作 + 代入もありません。私が何度も犯した間違いの 1 つreturn unless @started; @started = true
は、まったくスレッド セーフではない という記述です。
Ruby のスレッド セーフなステートメントとスレッド セーフでないステートメントの正式なリストは知りませんが、単純な経験則があります。式が 1 つの (副作用のない) 操作のみを実行する場合、それはおそらくスレッド セーフです。例: a + b
is ok, a = b
is also ok, and a.foo(b)
is ok,メソッドfoo
に副作用がない場合(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 アプリケーションを作成することもできます。他のgemは、スレッドセーフであると言わない限り、スレッドセーフではないと想定し、スレッドセーフであると言う場合は、そうでないと想定し、コードを調べます(ただし、次のようになることがわかったからです)@n ||= 1
スレッドセーフではないという意味ではありません。これは、適切なコンテキストで行うのが完全に正当なことです-代わりに、グローバル変数の可変状態、メソッドに渡された可変オブジェクトの処理方法、特にそれがどのように処理されるかなどを探す必要がありますオプションハッシュを処理します)。
最後に、スレッドが安全でないことは推移的なプロパティです。スレッド セーフではないものを使用するものは、それ自体がスレッド セーフではありません。