Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyスレッドの使い方を徹底解説!初心者向けわかりやすいチュートリアル

Rubyにおけるスレッド(Thread)とは、プログラムの中で複数の処理を同時に実行するための仕組みです。スレッドを活用すれば、Rubyプログラムは複数のタスクを並行してこなせるようになり、全体の処理速度向上が期待できます。

具体的には、次のような場面で役立ちます。

  • 複数のファイルを同時に読み込む
  • 複数のWebリクエストを同時に処理する
  • 複数のAPIへ同時に接続する

スレッドを使ったプログラムは「マルチスレッド」と呼ばれ、作業をより速く完了できるようになります。

ただし、注意点がひとつあります。

MRI(Matz's Ruby Interpreter)と呼ばれる標準のRuby処理系では、I/Oバウンドなアプリケーションの場合にのみスレッドの恩恵を受けられます。この制限はGIL(Global Interpreter Lock/グローバルインタプリタロック)と呼ばれる仕組みに起因しています。

一方、JRubyやRubiniusといった代替のRuby処理系であれば、マルチスレッドの性能をフルに引き出すことが可能です。

そもそもスレッドとは?

スレッドとは、いわば「作業員」のような存在で、プログラムの実行単位のことです。すべてのプロセスには最低1つのスレッドが備わっており、必要に応じて自由に追加できます。

コード例も見たいところですが、その前に「CPUバウンド」と「I/Oバウンド」というアプリケーションの違いについて理解しておきましょう。

I/Oバウンドなアプリケーションとは

I/Oバウンドなアプリケーションとは、外部リソースの応答を待つ時間が発生するアプリケーションのことです。例としては以下のようなものが挙げられます。

  • APIリクエスト
  • データベース(クエリ結果の取得)
  • ディスクからの読み込み

スレッドは、リソースが利用可能になるまでの間、処理を一時停止することができます。つまり、あるスレッドが待っている間に別のスレッドが動いて作業を進められるため、待ち時間を無駄にしません。

I/Oバウンドなアプリの典型例がWebクローラーです。

クローラーはリクエストごとにサーバーの応答を待たなければならず、待っている間は他の作業ができません。

しかし、スレッドを使えば話は変わります。

4つのリクエストを同時に送信し、レスポンスが返ってきた順に処理していけば、ページをより速く取得できるのです。

それでは、いよいよコード例を見ていきましょう。

Rubyスレッドの作成方法

Rubyで新しいスレッドを作成するには、Thread.newを呼び出します。その際、スレッドで実行したいコードをブロックとして渡してください。

Thread.new { puts "hello from thread" }

とても簡単ですね。

しかし、次のコードを実行すると、スレッドからの出力が表示されないことに気づくでしょう。

t = Thread.new { puts 10**10 }
puts "hello"

問題は、Rubyがスレッドの終了を待たないという点にあります。

上記のコードを修正するには、スレッドオブジェクトに対してjoinメソッドを呼び出します。

t = Thread.new { puts 10**10 }
puts "hello"
t.join

複数のスレッドを作成したい場合は、それらを配列に格納し、各スレッドに対してjoinを呼び出しましょう。

例:

threads = []

10.times {
  threads << Thread.new { puts 1 }
}

threads.each(&:join)

Rubyスレッドについてさらに詳しく調べたいときは、公式ドキュメントが役立ちます。

https://ruby-doc.org/core-2.5.0/Thread.html

スレッドと例外処理

スレッド内で例外が発生すると、そのスレッドはプログラムを停止させることも、エラーメッセージを表示することもなく、静かに終了してしまいます。

実際に例を見てみましょう。

Thread.new { raise 'hell' }

デバッグの観点からは、問題が発生した時点でプログラムを止めてほしいケースもあるはずです。その場合は、Threadクラスの次のフラグをtrueに設定します。

Thread.abort_on_exception = true

このフラグは、必ずスレッドを作成する前に設定してくださいね 🙂

スレッドプールでスレッド数を制御する

数百件ものアイテムを処理しなければならない場合、それぞれに対して個別にスレッドを起動すると、システムリソースを一気に消費してしまう恐れがあります。

イメージとしては、次のようなコードになります。

pages_to_crawl = %w( index about contact ... )

pages_to_crawl.each do |page|
  Thread.new { puts page }
end

この書き方だと、サーバーに対して何百もの接続を張ることになってしまい、あまり良いやり方とは言えません。

そこで有効なのがスレッドプールです。

スレッドプールを使えば、任意の時点で稼働するスレッドの数を制御できます。

自前でプールを実装することも可能ですが、おすすめはしません。次の例では、celluloid gemを利用して実現しています。

注意:Celluloidは現在メンテナンスされていませんが、「ワーカープール」という考え方自体は今でも十分通用します。

require 'celluloid'

class Worker
  include Celluloid

  def process_page(url)
    puts url
  end
end

pages_to_crawl = %w( index about contact products ... )
worker_pool    = Worker.pool(size: 5)

# 戻り値を収集したい場合は 'futures' をチェックしてみてください
pages_to_crawl.each do |page|
   worker_pool.process_page(page)
end

この方法なら、同時に動くのは5つのスレッドだけです。各スレッドは処理を終えると、自動的に次のアイテムを取りに行きます。

なお、Celluloidの代わりとなる現代的な選択肢としては、concurrent-rubyが提供するThreadPoolExecutorなどもあります。

レースコンディションなどの落とし穴

ここまでの内容は非常に魅力的ですが、コード中に安易にスレッドを散りばめる前に、並行処理にはいくつかの問題が伴うことを知っておく必要があります。

たとえば、スレッドはレースコンディション(競合状態)を起こしやすいのです。

レースコンディションとは、処理の実行順序が意図せず崩れてしまい、データがめちゃくちゃになる現象のことです。

もうひとつの問題がデッドロックです。これは、あるスレッドがミューテックスなどのロック機構によってリソースへの排他的アクセスを保持したまま解放せず、結果として他のすべてのスレッドがそのリソースにアクセスできなくなる状態を指します。

こうした問題を避けるには、生のスレッドを直接扱うのは避け、細かな部分をよしなに処理してくれるgemに任せるのがベストです。

その他のおすすめ並行処理gem

スレッドプールではcelluloidを使いましたが、並行処理に特化したgemは他にもたくさんあります。ぜひチェックしてみてください。

  • https://github.com/grosser/parallel
  • https://github.com/chadrem/workers
  • https://github.com/ruby-concurrency/concurrent-ruby

以上です!Rubyスレッドについて、何かしら学べたのであれば嬉しい限りです。

この記事が役に立ったと感じたら、ぜひ友人にもシェアして、一緒に学んでもらいましょう 🙂

  1. Rubyのエイリアス(別名定義)完全ガイド:aliasキーワードとalias_methodの違い

    Rubyでは、既存のメソッドに別名(エイリアス)を付ける方法が2つあります。 alias(キーワード) alias_method(メソッド) どちらも同じ目的で使えますが、挙動が微妙に異なるため、初心者にとって混乱しやすいトピックです。 本記事では、両者の違いを詳しく掘り下げ、しっかりと理解できるように解説していきます。 aliasキーワードとは まずはaliasから見ていきましょう。aliasはRubyのキーワードの一つです(ifやdef、classなどと同じ扱いです)。 基本的な書き方は以下の通りです。 alias print_something puts print_someth

  2. RubyのStructとOpenStructの使い方を徹底解説!値オブジェクトを簡単に作る方法

    Rubyには、関連する属性をまとめて保持する「値オブジェクト」を手軽に作れるStructという組み込みクラスが用意されています。本記事では、Structの基本的な使い方から注意点、そしてOpenStructとの違いまで詳しく解説します。 そもそもStructとは何か? StructはRubyに組み込まれたクラスで、新しいクラスを動的に生成し、値オブジェクト(Value Object)を作成するために使われます。値オブジェクトとは、関連性のある複数の属性をひとまとめにして扱うためのオブジェクトです。 具体例を挙げてみましょう。 2つの座標(xとy)を持つPointというデータを表したい場合、この