[Bash] 並列数を制限しつつコマンドを並列実行するパターン

次のスクリプトは1回1秒かかるコマンド(関数)を単純に直列実行する例です。ループで10回実行するため、全て終わるまでに10秒かかります。

sleep_then_echo() {
  sleep 1
  echo $1
}

for i in $(seq 1 10); do
  sleep_then_echo $i
done

これを並列実行して速くする方法を考えてみます。

並列数を無制限にバックグラウンドジョブとして並列実行する場合

これを &wait を使って並列実行すれば、10並列で実行されるため約1秒で終わります。

sleep_then_echo() {
  sleep 1
  echo $1
}

for i in $(seq 1 10); do
  sleep_then_echo $i &
done

wait

時間のかかるコマンドの末尾に & を付けることでバックグラウンドジョブとして非同期に実行し、wait コマンドで実行中の全てのジョブが終了するまで待機しています。

並列数を制限しつつバックグラウンドジョブとして並列実行するパターン

&wait だけで手軽に並列実行の恩恵を受けられますが、もし並列実行する処理が重たいものだった場合、並列数を多くしすぎると実行負荷が上がり、逆に遅くなったり不安定になる場合があります。

こういった場合は次のようにして並列数を制限することで、安定して実行できるようになります。10回のループを5並列に実行するため、約2秒で終わります。

# 同時実行するジョブの最大数
MAX_CONCURRENT_JOBS=5

sleep_then_echo() {
  sleep 1
  echo $1
}

# 実行しているジョブ数を出力する
running_jobs_count() {
  # -r は running のジョブだけを出力するオプション
  jobs -r | wc -l
}

for i in $(seq 1 10); do
  # 実行しているジョブが最大数に達している場合は終了を待機する
  while (( $(running_jobs_count) >= MAX_CONCURRENT_JOBS )); do
    sleep 1
  done

  sleep_then_echo $i &
done

wait

ちなみに各ジョブの成功可否 (exit code) を判定したい場合は、もう少し書き方を工夫する必要があります。

xargs で並列数を制限しつつ並列実行するパターン

for を使わないパターンとして、xargs -P number でも手軽に並列実行ができます。-P オプションで並列実行するプロセス数を指定できます。次のスクリプトは10回のループを5並列に実行するため、約2秒で終わります。

sleep_then_echo() {
  sleep 1
  echo $1
}

# xargs で起動した子プロセスからも関数を使用出来るようにする
export -f sleep_then_echo

# sh -c ではなく bash -c としないと環境によって期待通りに動作しない点に注意
seq 1 10 | xargs -I@ -P5 -n1 bash -c "sleep_then_echo @"

また、こちらの方法は並列実行した各コマンドのいずれかが失敗した場合、xargs の exit code も1以上(失敗)になります。

そのため set -eo pipefail しておくだけでお手軽にエラーハンドリングができる点もメリットです。(参考: シェルスクリプトを高級言語のような書き味に近づける Tips 集

感想

for で並列実行するパターンは説明的なスクリプトを書きやすいところがメリットだと思います。

xargs を使うパターンはオプションで並列数を指定するだけなので簡潔かつエラー検知も容易ですが、xargs の挙動やオプションを理解して使う必要があります。

また GNU parallel を使う方法もありますが、こちらは別途インストールが必要なので割愛。

GitHub が公開した Rails の DB アダプタ「trilogy」の特徴を調べた

Ruby on Rails の MySQL アダプタは現状 mysql2 gem が最もメジャーだと思いますが、社内チャットで以下の記事を見かけて GitHub が Ruby on Rails 向けの MySQL アダプタを公開していたことを知ったので、その特徴を軽く調べました。

trilogy の特徴

  • libmariadb / libmysqlclient への依存の回避
    • mysql2 gem と比較してインストールが容易
  • ActiveRecord アダプタもリリースされている (github/activerecord-trilogy-adapter)
    • Requirements Ruby 2.7 or higher
  • ネットワークパケット構築・解析のメモリ効率の向上
  • 動的メモリ割り当てによるメモリ効率の向上
  • 独自の低レベルネットワークプロトコル実装
  • implemented in C
  • MIT license

所感

2015年から GitHub 社内で使用されていたものがオープンソースとして公開されたもので、まだ GitHub 社外での利用実績は少なく、ライブラリが枯れるまでしばらくは待ちが懸命と思われます。

開発の経緯としては外部ライブラリへの依存を減らし、mysql2 gem よりも移植性と効率性を高めたい狙いがあったようです。個人的には注目。

関連

[Go] ポインタを取り扱う記号の紛らわしさ

ポインタを取り扱うコードって難しいですよね。プログラミングを習得する上で一つの鬼門になっているようです。

なぜポインタを取り扱うコードは難しいのか、それは文法の記号の紛らわしさが一因になっていると思います。

ポインタを取り扱うコードを書く時に *& どちらを書けばいいんだっけ?と迷ったことはありませんか?私はいつも迷います。

なぜ *& のどちらかを書くかで迷うのか

Go のコード例を示しつつ、理由を考察してみます。

以下の text は string 型の変数で、textRef は string のポインタ型の変数を表します。

var text string
var textRef *string

これらの変数に代入をしてみましょう。

// まずは普通に代入
text = "コーヒー"

// & を付けることで text を指し示すポインタとして代入する
textRef = &text

次にこれらの変数を使う時のことを考えてみます。
fmt.Println() で各変数の値を出力してみます。

// 出力: コーヒー
fmt.Println(text)

// 出力: 0xc000010230 (変数 text を指し示すアドレス)
fmt.Println(textRef)

// 出力: コーヒー (* を付けることで上のアドレスが指し示す先の値を得られる)
fmt.Printf(*textRef)

Go のポインタの基本文法は以上です。
ポインタを取り扱うための記号は基本的に &* の二つだけです。

二つの記号の意味を整理します。

* (アスタリスク) & (アンパサンド)
型を記述する時 型名の前に * を付けることでポインタ型を表す
変数を参照する時 変数名の前に * を付けることでポインタの先の値を表す 変数名の前に & を付けることでその変数のポインタを表す

同じ記号でも使う場面によって意味が異なる点に気がつくと思います。

string をポインタ型にしたいと思ったときに型名の先頭に * につける一方で、
ポインタ変数を string にしたいと思ったときも変数名の先頭に * を付けるのです。

感覚的な話になりますが * は使う場面によって正反対の意味を持つ印象を受けると思います。

私は未だにこれで取り違えることがありますが、なぜ紛らわしいのか認識できているだけでも楽になると思ったので言語化してみました。

理解しやすい記号の覚え方があるといいなと思っていますが、未だに答えが出ていません。(gopls などの Language Server を使ってコーディングしていれば、間違った時に怒られてすぐ気がつけるので大きな支障はありませんが…。)