【Rails】gem使ってみたシリーズ 〜parallel編〜 並列処理で時間短縮
はじめに
先日、Ruby on Rails のアプリケーションで、データを集計し、レポートを作成するジョブの実装をしました。
1つ1つの集計はそれほど時間がかからないのですが、複数の集計を行うため、全体の処理時間が長くなってしまいました。
そこで、parallel という gem を使って並列処理を行ったところ、処理時間を大幅に短縮することができました。
とても便利な gem なので、今回はその使い方をご紹介します。
対象読者
注意点
環境
並列処理とは
並列処理とは、コンピューターが複数の CPU コアで、複数の処理を同時に実行することを指します。
複数の処理を同時に実行することで、全体の処理時間を短縮することができます。
似たような用語に並行処理がありますが、これは異なる意味を持ちます。
並行処理は、複数の処理が同時に実行されるように見えますが、実際には1つの CPU コアでタスクを切り替えながら実行することを指します。
並列処理、並行処理については、以下の記事が参考になりました。
セットアップ
まずは、Gemfile に parallel を追加し、gem をインストールします。
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 を使う前と後で、処理時間を比較してみます。
並列処理を使わない場合
まずは、並列処理を使わない場合のコードを実行してみます。
# 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 を使って並列処理を行う場合のコードを実行してみます。
# 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 を使うことで、簡単に並列処理を行うことができ、処理時間を大幅に短縮することができました。
並列処理は、特に大量のデータを扱う場合や、時間のかかる処理を行う場合に非常に有効です。
ぜひ試してみてください。
最後までご覧いただき、ありがとうございました。
Discussion
ちなみにですが…
frozen_string_literal
は既定で有効になりました。コメントいただきありがとうございます!
こちらの方がスッキリしていて見やすいですね!
私が RuboCop の設定をきちんとしていなく、自動修正をした際に追加されていました!
現在は書かなくてもいいかもしれないですね!