mattn:フレームワークに頼って実装していると、そのフレームワークが内部でどの様な仕組みで並列または並行処理しているのかが理解できず、ただ使っているだけの状態になり得ます。 フレームワークの設計者からすると、プログラマがそれらを気にしなくても利用できるというのがプロジェクトのゴールでもあるので、それはそれで正しいのですが「並列処理」や「並行処理」を理解したいというモチベーションでは逆にそれが邪魔をしてしまうかもしれません。 並行処理や並列処理を学ぶのであれば、API サーバ等といった物ではなく、コード片で学び始めるのが良いと思います。 例えば Ruby で Thread と Fiber を使って簡単な処理を実装してみるのが理解を早めると思います。 ※ Go でも並行処理を学ぶことはできますが、Go の goroutine はスレッドでありスレッドではない、という特性があるため、並列処理や並行処理がどういった物かを学び始めるには難しいかもしれません。 まずは Fiber を使ったコードです。 def print_chars(s) Fiber.new do s.each_char do |c| Fiber.yield c end end end fiber = print_chars('あいう') puts fiber.resume puts fiber.resume puts fiber.resume この Fiber を使ったコードを実行すると「あ」「い」「う」が3つ表示されます。これのどこが並行処理なんだと思うかもしれませんが、並行処理は「実行状態を複数持つこと」なのです。 一方 Thread を使ったコードを示します。 require 'thread' def print_chars(a) q = Queue.new th = Thread.start do a.chars.each do |e| q.push e end end th.run q end v = print_chars('あいう') puts v.pop puts v.pop puts v.pop 同じく「あ」「い」「う」が表示されます。一見、同じ動作になる実装に見えますが、並行と並列には大きな違いがあります。これらの puts での表示処理を print_chars の中に移動してみましょう。 def print_chars(s) Fiber.new do s.each_char do |c| puts c Fiber.yield end end end fiber = print_chars('あいう') fiber.resume fiber.resume fiber.resume require 'thread' def print_chars(a) th = Thread.start do a.chars.each do |e| puts e end end th end th = print_chars('あいう') th.run th.join Fiber は、確かに実行環境が2つありますがそれを動かすためには resume を呼び出さなければなりません。一方で Thread は run を実行した後で勝手に動き出します。 例えば Fiber の方を、2つ同時に呼び出したい場合、それぞれの resume を呼び出さなければなりません。 fiber1 = print_chars('あいう') fiber2 = print_chars('かきく') fiber1.resume fiber2.resume fiber1.resume fiber2.resume fiber1.resume fiber2.resume Thread は実行してしまえば勝手に並列に実行されます。 require 'thread' def print_chars(a) th = Thread.start do a.chars.each do |e| puts e sleep 0.1 end end th end th1 = print_chars('あいう') th2 = print_chars('かきく') th1.run th2.run th1.join th2.join プログラムのパフォーマンスを大きく左右するのは I/O です。ですので Fiber の方は、fiber1 の I/O に伴って fiber2 の実行がブロックされる事になります。一方で Thread は片方のスレッドが I/O で待ちになっても、もう片方のスレッドが実行されます。 結果として、Ruby の Thread は I/O が絡む実装でパフォーマンスが良くなります。 ただし Ruby の Thread は、GIL (Global Interpreter Lock) があるため、安全にスレッド内で変数を触る事ができますが、完全に排他制御されるわけではありません。 class Counter attr_accessor :count, :tmp def initialize @count = 0 @tmp = 0 end def increment @count += 1 end end c = Counter.new t1 = Thread.start { 1000000.times { c.increment; c.tmp += 1 if c.count.even?; } } t2 = Thread.start { 1000000.times { c.increment; c.tmp += 1 if c.count.even?; } } t1.join t2.join p c.count # 常に 2000000 p c.tmp # 100000 でない事もある これを実行すると最終的な値が 1 だったり 997 だったり 1000 だったりします。 ※ これを回避するには Mutex を使わなければなりません。 また Ruby の GIL は I/O が発生するとそのタイミングで競合が起きえます。 当然ながら排他処理を入れると、プログラムが遅くなってしまいます。 処理を同時に実行したいというニーズに「並列」を使うか「並行」を使うかは、要件次第という事が分かると思います。新しい Ruby では Ractor という機能も追加されているため Thread よりも気軽に並列処理を扱う事ができると思います。 r1 = Ractor.new 'あいう'.chars do |arr| puts arr.each(&:to_s) end r2 = Ractor.new 'かきく'.chars do |arr| puts arr.each(&:to_s) end r1.take r2.take ※ これを実行すると混ざって表示される様になります。(タイミングによります) ぜひこういったコード片で仕組みを学ばれるのが良いと思います。 ちなみに、Fiber の例を実行されたのであれば「じゃぁ、Fiber の resume をもっと賢く呼んでくれたらいいのに、例えば fiber1 が I/O でブロックしている間に fiber2 の resume を呼び出し続けてくれたらいいのに」と感じたかもしれません。これを上手く扱える様にしているプログラミング言語も存在します。その1つが Go です。(Go の宣伝ではありません)(もっと読む)