シェルスクリプトで並列処理

なんだか検索件数が多いので、シェルスクリプトによるコンカレンシーのお話をしよう。

ただし、bad design (変数を書き換えるとか、相互にやりとりするとか)は除外する。

また、Zshを前提とする。

投げっぱなし

まず基本。投げっぱなしはとても簡単。

シェルスクリプトではジョブコントロールは無効になっているので、SIGHUPの送信はなされないので、さっさと終了してしまっても大丈夫。

処理の終了を待ちたい場合はwait

flock

flockを使う方法は簡単でシンプル。 ロックファイルを使ってファイルデスクリプタを開きっぱなしにし、そのファイルデスクリプタを指定してロックする。

まずファイルデスクリプタ9.lockファイルをライトモードでオープンする場合

そしてファイルデスクリプタ9を閉じる場合

これを利用するとこんな感じ。

ロックしている間に共有しているリソースの読み込み/変更を行い、ファイルデスクリプタを閉じる。

リソースを読むより簡単な方法は、ひとつのストリームを共有したファイルデスクリプタとして開き、 ロックを中に読むことである。

ワーカーを生成するサブシェルの標準入力はqueueファイルにリダイレクトされている。 そのため、ファイルデスクリプタ0queueファイルなのだが、そのサブシェルの子プロセスであるワーカープロセスはリダイレクトしていないため、このファイルデスクリプタが共有される。 結果、全てのワーカーはqueueファイルを標準入力とするのだが、ストリーム自体を共有しているため、どのワーカーが一行読んだとしてもストリームの位置が変更され、次に読み込む位置は他のワーカーにとっても変更される。実際

WORKER 1: foo
WORKER 2: bar
WORKER 3: baz

となる。

producer-consumer キュー

もっと凝ったことがしたいのであればUNIXドメインソケットを使ってproducer部分をシングルスレッド化することができる。

zsocket -lのタイミングで接続を受け付けているのだが、zsocket -lしていないタイミングでは接続しようとするプロセスをブロックするため、producer側の処理は直列に行われる。

双方向性があるときはproducerと直接やりとりできるのはメリット。

Orbit designの場合

私が採用しているOrbit designはレギュレーターはZshスクリプトなので、基本的にこのような並列化手順をとっている。 とはいえ、ものによっては直列(serial)になっていたりする。

ただ、並列化されているものが多い。ワーカープロセスは最も多いもので5。

基本的にワーカースクリプトが受け取るのは処理対象ID(ほとんどの場合ファイルパス)だけである。 それ以外の情報はスクリプト側で生成するか、別途取得するかする。