2024年11月22日金曜日

Ruby で Thread によるシングルコアとマルチコア化について

Ruby で Thread によるシングルコアとマルチコア化について

概要

Ruby は GVL の関係で基本はスレッド化してもシングルコアでしか動きません
てっとり早くマルチコア化した場合は parallel を使います

環境

  • macOS 15.1.1
  • Ruby 3.3.5
  • parallel 1.26.3

Thread 化する前のコード

配列を単純に順番に処理する関数です

スレッド化することを前提にコードを作成しており add はインスタンス変数の配列にデータを登録するよういになっています
またインスタンス変数は基本的に順番を保証しないことを考慮した作りにしたほうがいいです (Thread でどんどん処理されるので)

def parse_and_add_tweet(full_text)
  tweet = Tweet.new(full_text, @mecab)
  tweet.analyze
  add(tweet)
end

def exec
  @data.each do |d|
    parse_and_add_tweet(d['tweet']['full_text'])
  end
end

通常の Thread

配列のデータをスライスしてスライスしたデータをスレッドに渡して処理させるようなコードに変更します

thread_batch_size は作成するスレッド数ごとにデータが均等に割り当たるようにしています

def thread_batch_size
  (@data.size / thread_count.to_f).ceil
end

def thread_count
  32
end

def parse_and_add_tweet(full_text)
  tweet = Tweet.new(full_text, @mecab)
  tweet.analyze
  add(tweet)
end

def analyze
  threads = []
  @data.each_slice(thread_batch_size) do |sliced_data|
    threads << Thread.new do
      sliced_data.each do |d|
        parse_and_add_tweet(d['tweet']['full_text'])
      end
    end
  end
  threads.each(&:join)
end

これで実行するとわかりますがシングルコアの CPU を100%使用しますが残りのコアは何もしていないことが確認できます

parallel を使う

parallel_process_count は CPU コア数になります
こちらの方が単純に書くことができます
また実行するとすべての CPU が使われ 100% になっていることが確認できると思います

def parallel_process_count
  Parallel.processor_count
end

def parse_and_add_tweet(full_text)
  tweet = Tweet.new(full_text)
  tweet.analyze
  add(tweet)
end

def analyze
  @tweets = Parallel.map(@data, in_processes: parallel_process_count) do |d|
    parse_and_add_tweet(d['tweet']['full_text'])
  end
end

parallel を使う際は少し工夫が必要なのでそれに関しては後述のトラブルシューティングに記載しています

結果比較

通常 の Thread

1775.98s user 2.64s system 99% cpu 29:40.44 total

parallel を使う

3552.51s user 269.73s system 701% cpu 9:04.50 total

3-4倍くらい速くなりました

トラブルシューティング

'dump': no _dump_data is def ined for class FFI::Pointer (TypeError)

parallel にわたすブロックに Marshal.dump できない値は渡せません
今回で言うと mecab オブジェクトが FFI::Pointer をクラスを使っており渡せないので mecab オブジェクトの生成は Tweet クラス側の initialize ではなく各種関数側で行うように修正しました

最後に

  • そんなに処理時間がかからない -> 通常のループ
  • シングルコアは MAX に使いたい -> Thread を使ったループ
  • マルチコアであるだけリソースを使いたい -> parallel を使ったループ

という感じで使い分けるといいかなと思います
parallel の場合ほぼリソースを食い尽くすことができてしまうので他のプロセスの邪魔にならないように注意しましょう

参考サイト

0 件のコメント:

コメントを投稿