🔱

【Rails】gem使ってみたシリーズ 〜parallel編〜 並列処理で時間短縮

に公開2

はじめに

先日、Ruby on Rails のアプリケーションで、データを集計し、レポートを作成するジョブの実装をしました。

1つ1つの集計はそれほど時間がかからないのですが、複数の集計を行うため、全体の処理時間が長くなってしまいました。

そこで、parallel という gem を使って並列処理を行ったところ、処理時間を大幅に短縮することができました。

とても便利な gem なので、今回はその使い方をご紹介します。

https://rubygems.org/gems/parallel

https://github.com/grosser/parallel

対象読者

注意点

環境

並列処理とは

並列処理とは、コンピューターが複数の CPU コアで、複数の処理を同時に実行することを指します。

複数の処理を同時に実行することで、全体の処理時間を短縮することができます。

似たような用語に並行処理がありますが、これは異なる意味を持ちます。

並行処理は、複数の処理が同時に実行されるように見えますが、実際には1つの CPU コアでタスクを切り替えながら実行することを指します。

並列処理、並行処理については、以下の記事が参考になりました。

https://www.zunouissiki.com/concurrent-parallel-diff/

https://www.asobou.co.jp/blog/web/concurrent-parallel

セットアップ

まずは、Gemfile に parallel を追加し、gem をインストールします。

Gemfile
gem 'parallel'
ターミナル
docker compose exec app bundle install

特に設定などは必要ないので、セットアップは以上です。

コンソールで使用できる CPU コア数を確認しておきます。

ターミナル
docker compose exec app rails c
Loading development environment (Rails 7.0.8.6)
irb(main):001> Parallel.processor_count
=> 5

使用できる CPU コア数が 5 であることが確認できました。

これは Docker の設定によるものです。

ここの設定を変更することで、使用できる CPU コア数を変更することができます。

使用例

では、実際に parallel を使ってみます。

今回は、parallel を使う前と後で、処理時間を比較してみます。

並列処理を使わない場合

まずは、並列処理を使わない場合のコードを実行してみます。

lib/tasks/parallel_test.rake
# frozen_string_literal: true

namespace :test do
  desc 'parallel_test'
  task parallel_test: :environment do
    start_time = Time.now

    items = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]

    items.each do |item|
      (1..100).each do |i|
        puts "#{item} - #{i}"
        sleep(0.1)
      end
    end

    end_time = Time.now

    elapsed_time = end_time - start_time

    puts "実行時間:#{elapsed_time.round(2)}秒"
  end
end

こちらが並列処理を使わない場合のコードです。

A から J までのアルファベットをループして、1 から 100 までの数字を出力し、全てのアルファベットのループが終わったら、最後に実行時間を出力します。

ターミナル
docker compose exec app bundle exec rake test:parallel_test

A - 1
A - 2
A - 3
A - 4
A - 5
•••
A - 96
A - 97
A - 98
A - 99
A - 100
B - 1
B - 2
B - 3
B - 4
B - 5
•••
J - 96
J - 97
J - 98
J - 99
J - 100

実行時間:101.57

このような結果になりました。

並列処理を使う場合

次に、parallel を使って並列処理を行う場合のコードを実行してみます。

lib/tasks/parallel_test.rake
# frozen_string_literal: true

namespace :test do
  desc 'parallel_test'
  task parallel_test: :environment do
    start_time = Time.now

    items = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]

+   Parallel.map(items) do |item|
      (1..100).each do |i|
        puts "#{item} - #{i}"
        sleep(0.1)
      end
    end

    end_time = Time.now

    elapsed_time = end_time - start_time

    puts "実行時間:#{elapsed_time.round(2)}秒"
  end
end

こちらが parallel を使って並列処理を行う場合のコードです。

items.each do |item| の部分を Parallel.map(items) do |item| に変更するだけなので、とても簡単に実装できました。

ターミナル
docker compose exec app bundle exec rake test:parallel_test

B - 1
A - 1
C - 1
D - 1
E - 1
C - 2
D - 2
E - 2
B - 2
A - 2
D - 3
E - 3
B - 3
A - 3
C - 3
C - 4
E - 4
D - 4
B - 4
A - 4
D - 5
E - 5
A - 5
B - 5
C - 5
•••
D - 96
B - 96
A - 96
C - 96
E - 96
B - 97
E - 97
A - 97
D - 97
C - 97
E - 98
A - 98
D - 98
C - 98
B - 98
E - 99
A - 99
B - 99
D - 99
C - 99
E - 100
A - 100
C - 100
B - 100
D - 100
F - 1
G - 1
H - 1
I - 1
J - 1
H - 2
G - 2
J - 2
I - 2
F - 2
F - 3
G - 3
I - 3
J - 3
H - 3
I - 4
J - 4
H - 4
G - 4
F - 4
J - 5
G - 5
H - 5
I - 5
F - 5
•••
I - 96
G - 96
H - 96
J - 96
F - 96
G - 97
I - 97
H - 97
F - 97
J - 97
I - 98
G - 98
H - 98
J - 98
F - 98
H - 99
F - 99
I - 99
J - 99
G - 99
F - 100
H - 100
J - 100
G - 100
I - 100

実行時間:20.37

このような結果になりました。

先ほど確認した通り、使用できる CPU コア数は 5 なので、5 つの処理が同時に実行されていることがわかります。

処理時間も約 20 秒と、並列処理を使わない場合の約 100 秒から約 5 分の 1 に短縮されました。

まとめ

parallel を使うことで、簡単に並列処理を行うことができ、処理時間を大幅に短縮することができました。

並列処理は、特に大量のデータを扱う場合や、時間のかかる処理を行う場合に非常に有効です。

ぜひ試してみてください。

最後までご覧いただき、ありがとうございました。

GitHubで編集を提案
株式会社L&E Group

Discussion

砂漠砂漠

ちなみにですが…

  • AからJの配列をつくるときは以下のようにも書けます。
("A".."J").to_a
RyoyaOkumaRyoyaOkuma

コメントいただきありがとうございます!

AからJの配列をつくるときは以下のようにも書けます。

こちらの方がスッキリしていて見やすいですね!

Ruby 3.4からはマジックコメントfrozen_string_literalは既定で有効になりました。

私が RuboCop の設定をきちんとしていなく、自動修正をした際に追加されていました!
現在は書かなくてもいいかもしれないですね!