プログラミングをやっていれば「マルチスレッド処理」や「並列処理」という言葉を耳にした方も少なくないでしょう。これらは、複数の処理を同時に実行するときに用い、Webサーバーや、効率的な処理が求められるシステムでは、このような同時実行ができるような仕組みが用いられています。
Rubyで複数の処理を同時に実行する場合は「Thread」クラスを使用します。この記事では、Rubyで「Thread」クラスを作成して、複数の処理を同時実行する方法などを紹介していきます。
スレッドとは?
そもそもスレッドとは、プログラムの一連の処理のまとまりです。通常、プログラムを実行すると1つのスレッド(メインスレッド)が作成され、1つのスレッドの中ではプログラムは記述した順に実行されます。また、1つのスレッドの中では同時に複数の処理は実行できません。
例えば、Webサーバー上で動くアプリケーションの場合では、不特定多数のユーザーからのアクセスを処理しなければなりませんが、1つのスレッドですべてのユーザーの要求を順番に処理しているようでは、とてもではありませんが処理が間に合いません。
そこで「Thread」クラスの出番です。
Rubyに限らず多くのプログラミング言語では、並列処理のための仕組み(API)が用意されています。もちろんRubyも例外ではなく、並列処理の為の「Thread」クラスが用意されており、このクラスを使うことで並列処理を簡単に実装できます。
この「Thread」クラスは、このスレッドを複数作成するためのクラスで、作成した「Thread」クラスの数だけ同時に処理が実行できます。
以降はサンプルコードを作りながら「Thread」クラスの使い方を見ていきましょう。
Threadを作成する
スレッドを作るには、Thread#newを使います。また、スレッドの中で実行する処理をブロックで指定します。
次の例は、Thread#newで作成した子スレッドと、メインスレッドでそれぞれ現在時刻を出力するサンプルコードです。
th = Thread.new do
p Time.now
sleep(2)
end
p Time.now
子スレッドには時刻の出力後、2秒のsleepがあるが、それぞれスレッドは並列に実行されるため、出力結果には同じ時刻が出力されます。
もし、上の処理をスレッドを使わずに(シングルスレッド)で次のように書いた場合、当然ですが、出力結果の時刻は2秒遅れで出力されます。
p Time.now
sleep(2)
p Time.now
2021-05-12 14:02:48.386885916 +0000
2021-05-12 14:02:50.387974976 +0000
Thread#joinでスレッドの終了を待ち合わせる
スレッドは基本的には並列で実行を行いますが、時には1つまたは複数のスレッドが終了するまで処理を待ち合わせケースもあります。Thread#joinメソッドを使用すると、指定したスレッドの処理が終了するまでレシーバーの処理を待機させることができます。
次の例は、スレッドthが終了するまで、th.joinの行でメインスレッドの処理を中断させるサンプルコードです。
th = Thread.new do
puts "スレッドを開始 #{Time.now}"
sleep(3)
puts "スレッドを終了 #{Time.now}"
end
puts "開始 #{Time.now}"
th.join
puts "終了 #{Time.now}"
開始 2021-05-12 13:52:07 +0000
スレッドを開始 2021-05-12 13:52:07 +0000
スレッドを終了 2021-05-12 13:52:10 +0000
終了 2021-05-12 13:52:10 +0000
このように、Thread#joinメソッドを呼ぶと、そのスレッドが終了するまで、そこで処理が中断していることが分かります。
複数のスレッドの終了を待機する
複数スレッドを同時実行している時に、すべてのスレッドの終了まで待機したい場合もあるでしょう。
1つ1つのスレッドに対してThread#joinメソッドを呼ぶコード書いてもよいですが、次のように煩雑になり、スレッド数が今後増えるとすると、保守性が悪いコードになってしまいます。
th1 = Thread.new do
puts "スレッド1"
sleep(3)
end
th2 = Thread.new do
puts "スレッド2"
sleep(1)
end
#スレッド1,2が両方完了するまで待機
th1.join
th2.join
複数のスレッドの待ち合わせを行う場合は、スレッドのオブジェクトを配列で管理し、一括で待機処理を書いた方が、コードがスッキリし将来的にもメンテナンス性が高くなります。
threads = []
threads << Thread.new do
puts "スレッド1"
sleep(3)
end
threads << Thread.new do
puts "スレッド2"
sleep(1)
end
#eachメソッドを使って配列の中のスレッドが全て完了するまで待機
threads.each{|th| th.join()}
また、eachメソッドの呼び出し部分は、シンボルにして次のように書き換えることもできます。
threads.each(&:join)
Mutexでスレッド間の排他制御
Threadクラスで並列で動く処理を作るときに注意が必要なのが、共有リソースへの同時アクセス制御である。
次の例は、変数countをスレッドの中でインクリメントしていき、1〜10までの合計値を求めるコードです。
count = 0
sum = 0
threads = [*1..10].map do |i|
Thread.new do
if flag
count += 1
flag = false
end
end
end
threads.each(&:join)
p count
本来であれば1〜10までの合計は55であるが、並列でスレッドの処理が動き、countをインクリメントしたことにより、一気にcountの数値が上がってしまい合計値がおかしくなってしまいました。
このように、複数のスレッドから同時にアクセスされると問題になる変数などの共有リソースを扱う場合は、排他制御を用います。
RubyにはThread::Mutexクラスが用意されており、一般的にはこのクラスを使っててスレッド間の排他制御を行います。
Thread::Mutexクラスで排他制御することで、並列処理であっても共有リソースにアクセスする処理の時だは、シーケンシャルに処理を行い、共有リソースへの同時アクセスを防げます。
では、先程の1〜10までの合計値をThread::Mutexを使って排他制御してみましょう。
count = 0
sum = 0
threads = [*1..10].map do |i|
Thread.new do
if flag
count += 1
flag = false
end
end
end
threads.each(&:join)
p count
今度は正しく1〜10までの合計値として「55」が表示されました。
まとめ
Rubyの「Thread」クラスを使って、複数のスレッドを作成し、並列処理を実装する方法を解説してきました。
マルチスレッドによるプログラミングは、上から下へ逐次処理を行っていく、シングルスレッドのプログラミングに比べ、気をつけるべき点や、処理の複雑性が一気に上がります。
すぐに「Thread」クラスを使って効率的で安全な並列処理処理を作ることハードルが高いかもしれませんが、色んな処理をスレッドで作成してマルチスレッドマスターを目指していきましょう。
Thread::Mutexクラスを用いることで、並列処理の中で、一部の処理だけは同時に実行されないような制御ができます。