初心者でもマスター!Rubyによるマルチスレッドプログラミングの完全ガイド8選

マルチスレッドプログラミングのサンプルコードが表示されているコンピュータ画面Ruby
この記事は約13分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミングの世界で重要な役割を果たす「マルチスレッド」について、今回はRubyの言語を使用して解説します。

この記事では、マルチスレッドの基本からRubyでの使用方法、実践的な応用例まで、初心者にも理解しやすいように詳しく紹介します。

さらに、各部分でサンプルコードを交えて、具体的な使い方を学べます。

●Rubyとマルチスレッドとは

Rubyは、シンプルさと高い生産性を重視したプログラミング言語です。

マルチスレッドとは、複数のスレッド(仕事の単位)を同時に処理することで、プログラムの効率を向上させる手法です。

○マルチスレッドの基本

マルチスレッドプログラミングでは、一つのプログラム内で複数のスレッドを並行して実行します。

これにより、一部のスレッドがブロック(待機)状態になっても、他のスレッドは引き続き実行されるため、プログラム全体の効率を向上させることが可能となります。

●Rubyでのマルチスレッドの使い方

Rubyには、マルチスレッドプログラミングを行うためのThreadクラスが用意されています。

Thread.newメソッドを使用して新しいスレッドを作成し、スレッド内で実行したい処理をブロックとして記述します。

○サンプルコード1:基本的なマルチスレッドの作成

このコードでは、Threadクラスを使って二つのスレッドを作成し、それぞれで”Hello, Thread!”と表示するコードを紹介しています。

この例では、二つのスレッドが並列に実行されて、各スレッドがそれぞれメッセージを表示しています。

# スレッドを2つ作成
thread1 = Thread.new do
  puts "Hello, Thread 1!"
end

thread2 = Thread.new do
  puts "Hello, Thread 2!"
end

# 並列実行
thread1.join
thread2.join

このコードを実行すると、”Hello, Thread 1!”と”Hello, Thread 2!”が出力されます。

ただし、マルチスレッドの性質上、表示の順番は実行ごとに変わる可能性があります。

○サンプルコード2:スレッド間でデータを共有する

次に、スレッド間でデータを共有する方法を見ていきましょう。

Rubyでは、スレッド間でデータを共有するために、グローバル変数やインスタンス変数を利用できます。

このコードでは、共有されたデータを使って、二つのスレッドがそれぞれ計算を行う例を紹介しています。

この例では、共有されたデータを使ってスレッドがそれぞれ計算を行っています。

# 共有されるデータ
data = 0

# スレッドを2つ作成
thread1 = Thread.new do
  data += 1
end

thread2 = Thread.new do
  data += 2
end

# 並列実行
thread1.join
thread2.join

puts data

このコードを実行すると、dataの初期値が0で、thread1とthread2でそれぞれ加算を行うため、最終的なdataの値は3となります。

このようにスレッド間でデータを共有することで、より複雑な並列処理を実現することができます。

○サンプルコード3:スレッドの同期

スレッド間でデータを共有する場合、同じデータに対して複数のスレッドが同時に書き込むことを避けるために、スレッドの同期が必要となります。

このコードでは、Threadクラスのsynchronizeメソッドを用いてスレッドの同期を取る例を紹介しています。

この例では、二つのスレッドが並列に実行されますが、synchronizeメソッドにより一つのスレッドが完了するまで他のスレッドは待つという処理を行っています。

# 共有されるデータ
data = 0
mutex = Mutex.new

# スレッドを2つ作成
thread1 = Thread.new do
  mutex.synchronize do
    data += 1
  end
end

thread2 = Thread.new do
  mutex.synchronize do
    data += 2
  end
end

# 並列実行
thread1.join
thread2.join

puts data

このコードを実行すると、dataの初期値が0で、thread1とthread2でそれぞれ加算を行いますが、mutex.synchronizeメソッドにより、一つのスレッドがdataを加算する間、他のスレッドは待つため、最終的なdataの値は3となります。

○サンプルコード4:マルチスレッドを活用したデータ処理

次に、マルチスレッドを活用して大量のデータを効率よく処理する方法を見ていきましょう。

このコードでは、複数のスレッドを生成して、それぞれのスレッドでリスト内のデータを加工する例を紹介しています。

この例では、大量のデータに対して各スレッドが並列に処理を行うことで、効率的にデータ処理を行っています。

# データリスト
data_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
results = []

# スレッドを作成してデータを加工
data_list.each do |data|
  Thread.new do
    results << data * 2
  end
end

# すべてのスレッドの完了を待つ
Thread.list.each do |t|
  t.join unless t == Thread.current
end

puts results

このコードを実行すると、data_listの各要素が2倍された新しいリストがresultsに格納され、その結果が出力されます。

マルチスレッドを活用することで、大量のデータを高速に処理することが可能となります。

●マルチスレッドの応用例

マルチスレッドプログラミングは、コンピュータリソースを効率的に使用するための重要な技術です。

一つのプロセス内で複数のスレッドを同時に実行することで、プログラムのパフォーマンスを大幅に向上させることが可能となります。

ここでは、Rubyでマルチスレッドを利用する具体的な応用例を紹介しています。

○サンプルコード5:並列処理による高速化

このコードでは、RubyのArrayクラスのmapメソッドを使って、配列内の全ての要素を並列に処理する例を紹介しています。

この例では、mapメソッドを使用して、配列の各要素に対して並列に処理を行い、その結果を新しい配列として返しています。

# 大量のデータ
data = Array.new(10000) { rand(1..100) }

# 並列処理による高速化
results = data.map do |num|
  Thread.new { num * 2 }
end.map(&:value)

puts results

このコードを実行すると、大量のランダムな整数が含まれる配列dataの各要素が2倍された新しい配列resultsが生成されます。

配列の大きさが大きい場合でも、マルチスレッドを用いることで効率的に計算を行うことができます。

○サンプルコード6:マルチスレッドによるリアルタイム処理

マルチスレッドは、リアルタイム処理のための最適なソリューションともなります。

このコードでは、Threadクラスの新しいインスタンスを作成して、それぞれのスレッドがリアルタイムに独立して処理を実行する例を紹介しています。

この例では、一つのスレッドが時間の経過と共にデータを生成し、もう一つのスレッドがそのデータをリアルタイムで収集する処理を行っています。

# データ生成スレッド
producer = Thread.new do
  while true
    sleep rand(1..2) # ランダムな時間を待つ
    puts "Produced data at #{Time.now}"
  end
end

# データ収集スレッド
collector = Thread.new do
  while true
    sleep 1 # 毎秒データを収集
    puts "Collected data at #{Time.now}"
  end
end

# スレッドが終了するまで待つ
[producer, collector].

each(&:join)

このコードを実行すると、’Produced data at’と’Collected data at’のメッセージがそれぞれ異なるペースで表示されます。

これにより、複数のスレッドが同時に独立した処理を行っていることがわかります。

●注意点と対処法

マルチスレッドプログラミングには多大な恩恵がありますが、それには注意点が伴います。

ここでは、「デッドロック」や「レースコンディション」という、マルチスレッドプログラミングでよく遭遇する問題点を解説し、それらを避けるための対策を提供します。

○デッドロックとは何か

デッドロックとは、複数のスレッドが共有リソースを取得しようとして、無限に待ち続ける状態を指します。

それぞれのスレッドが、他のスレッドが保持しているリソースを待っている間、そのスレッド自体が別のリソースを保持している状態がデッドロックとなります。

下記のサンプルコードは、デッドロックの発生する可能性があるシナリオを表しています。

# リソース
resource1 = Mutex.new
resource2 = Mutex.new

# スレッド1
thread1 = Thread.new do
  resource1.lock
  sleep 1
  resource2.lock
end

# スレッド2
thread2 = Thread.new do
  resource2.lock
  sleep 1
  resource1.lock
end

# スレッドの終了を待つ
[thread1, thread2].each(&:join)

このコードでは、2つのスレッドがそれぞれ2つのMutexオブジェクト(resource1とresource2)を取得しようとします。

しかし、スレッド1は最初にresource1を取得し、次にresource2を取得しようとします。

一方、スレッド2は最初にresource2を取得し、次にresource1を取得しようとします。

これにより、両スレッドが互いのリソースの解放を永遠に待つことになり、デッドロックが発生します。

○レースコンディションとは何か

レースコンディションとは、複数のスレッドが共有リソースに対して同時にアクセスしようとした結果、その実行結果がスレッドの実行順序によって変わってしまう現象を指します。

下記のサンプルコードは、レースコンディションが発生する可能性があるシナリオを表しています。

# 共有リソース
count = 0

# スレッド1
thread1 = Thread.new do
  10000.times do
    count += 1
  end
end

# スレッド2
thread2 = Thread.new do
  10000.times do
    count += 1


 end
end

# スレッドの終了を待つ
[thread1, thread2].each(&:join)

# 結果を出力
puts count

このコードでは、2つのスレッドがそれぞれ10000回ずつcount変数に1を加えます。

シングルスレッド環境では、この結果は必ず20000になるはずです。

しかし、マルチスレッド環境では、両スレッドが同時にcount変数を読み書きするとレースコンディションが発生し、期待する結果(20000)にならない可能性があります。

○それらを避けるための対策

以上の問題を避けるためには、次のような対策をとることが推奨されます。

  1. デッドロックを避けるためには、スレッドがリソースを取得する順序を一貫させることが有効です。
    上述のデッドロックの例では、スレッド1とスレッド2がリソースを取得する順序が逆でした。
    これを両スレッドともresource1を先に、次にresource2を取得するように統一すれば、デッドロックは避けられます。
  2. レースコンディションを避けるには、スレッドが共有リソースに同時にアクセスしないようにすることが重要です。
    これは、MutexやSemaphoreなどの同期メカニズムを利用して行うことができます。
    上述のレースコンディションの例では、count変数へのアクセスをMutexで保護することで、レースコンディションを避けることができます。

●カスタマイズ方法

Rubyのマルチスレッドプログラミングでは、さまざまなカスタマイズが可能です。

特に、スレッドの優先度設定や例外処理のカスタマイズは重要なテクニックであり、ここではその方法を詳しく解説します。

○サンプルコード7:マルチスレッドの優先度設定

スレッドの優先度は、スレッドが実行される順序を制御します。優先度が高いスレッドは、優先度が低いスレッドより先に実行されます。

下記のコードでは、スレッドの優先度を設定し、その実行順序を観察します。

# スレッド1
thread1 = Thread.new do
  Thread.current.priority = 1
  5.times do
    puts "スレッド1実行中"
    sleep 1
  end
end

# スレッド2
thread2 = Thread.new do
  Thread.current.priority = 0
  5.times do
    puts "スレッド2実行中"
    sleep 1
  end
end

# スレッドの終了を待つ
[thread1, thread2].each(&:join)

このコードでは、スレッド1の優先度を1に、スレッド2の優先度を0に設定しています。

その結果、スレッド1がスレッド2より優先されて先に実行されます。

○サンプルコード8:例外処理のカスタマイズ

マルチスレッドプログラミングでは、各スレッドが独立して実行されるため、一つのスレッドで発生した例外が他のスレッドに影響を及ぼさないように、適切な例外処理が必要となります。

下記のコードでは、スレッド内で例外が発生した場合の処理をカスタマイズしています。

# スレッド1
thread1 = Thread.new do
  begin
    # 0での除算で例外を発生させる
    1 / 0
  rescue => e
    puts "スレッド1で例外が発生: #{e.message}"
  end
end

# スレッド2
thread2 = Thread.new do
  5.times do
    puts "スレッド2実行中"
    sleep 1
  end
end

# スレッドの終了を待つ
[thread1, thread2].each(&:join)

このコードでは、スレッド1で0除算による例外が発生しますが、その例外は適切に処理され、スレッド2の実行には影響しません。

これにより、一部のスレッドで問題が発生しても他のスレッドが安全に実行されることを保証します。

まとめ

本ガイドでは、Rubyでのマルチスレッドプログラミングの基本から、詳細なカスタマイズ方法までを解説しました。

特に、スレッドの優先度設定と例外処理のカスタマイズについては、効率的で安全なマルチスレッドプログラムを作成する上で必須の知識となります。

Rubyのマルチスレッドプログラミングは、並列処理を行う上で非常に便利な機能です。

しかし、スレッド間でデータを共有する際や、複数のスレッドを同時に管理する際には注意が必要となります。

本ガイドで紹介した内容を理解し、適切なマルチスレッドプログラミングを行いましょう。

また、今後もマルチスレッドプログラミングの応用例や、より高度なテクニックについて学ぶことで、より複雑な問題に対応する能力を身につけることができます。

この旅は、初心者から始めても、一歩ずつ進むことで必ずマスターできます。

忍耐強く、確実にスキルを習得していきましょう。