6
8

More than 3 years have passed since last update.

bash の coproc について

Last updated at Posted at 2020-07-03

Overview

bash 4 から coprocess があるのを知ったので試してみます。
ざっくり言うと、メインプロセスと stdin/stdout を繋ぎながら、バックグラウンドでサブプロセスを起動できる機能です。

イメージ的には cmd & に近いですが、入出力が繋がってるのがイケてるポイントです。
具体的には、 coproc で起動すると、メインプロセス側の配列変数にファイルディスクリプタが割り当てられて、それがサブプロセスの stdin/stdout に繋がります。

こんな感じ
                 +-- ${COPROC[1]}  --> stdin
                 +                      ↓
[main process] --+                 [sub process &]
                 +                      ↓
                 +-- ${COPROC[0]}  <-- stdout       

cf.

詳しい使い方とかはこの辺見てください。

https://qiita.com/angel_p_57/items/7b54493e6e66aea01437
https://www.gnu.org/software/bash/manual/html_node/Coprocesses.html
https://unix.stackexchange.com/questions/86270/how-do-you-use-the-command-coproc-in-various-shells

あと bash と ksh zsh とかだと挙動が微妙に違うみたいです。
bash の縛りがきついw

usage

具体的に見た方がたぶん理解が早いので、

1000行の tsv を while read で 1行ずつ csv に変換する場合

を考えてみます。

使用したバージョン
$ echo $BASH_VERSION
5.0.17(1)-release

なお、サブプロセスの出力がバッファリングされると詰まるので、awk で試してみます。
1行ずつ処理する際のパフォーマンスを確認したいので、awk の stdin に直接流し込むとかは今は忘れてください。

coproc を使わないケース

1行ずつ読むたびに毎回 awk を起動するので、

nouse-coproc.sh
#!/bin/bash

seq -f '%04g' 3000 | paste - - - \
| while read x; do
    echo "$x" | awk -v OFS=, '{$1=$1; print $0; }'
done

けっこう時間がかかりますね。

$ time bash nouse-coproc.sh >/dev/null

real    0m2.685s
user    0m2.250s
sys     0m0.722s

coproc を使うと

use-coproc.sh
#!/bin/bash

coproc hoge { awk -v OFS=, '{ $1=$1; print $0; fflush(); }' ; }

while read x; do
    echo "$x" >&${hoge[1]}
    read -u ${hoge[0]} a
    echo "$a"
done < <(seq -f '%04g' 3000 | paste - - -)

めっちゃ速くなります。

$ time bash use-coproc.sh >/dev/null

real    0m0.079s
user    0m0.054s
sys     0m0.023s

なんで速くなるか

もう上に答え書いちゃったんですけど、
1行ずつ毎回プロセスを起動するときのオーバヘッドが遅くなる原因です。
このオーバヘッドは入力行数が増えれば増えるほど線形に増大していきます。

それに対して、 coproc は最初に1つだけプロセスを起動して、
1行ずつ stdin に流し込んで、処理結果を stdout から read していくので、
普通に stdin からコマンドに流し込むのと同じ効果があります。

シェル芸で1行ずつ処理する場合に捗りそうですね。

appendix

ファイルディスクリプタがプロセスをまたいで引き継げない問題

たぶん bash 縛りだと思うんでうけど、
サブプロセスにはファイルディスクリプタが引き継げず、直で fd 値を指定しても 不正なファイル記述子です みたいなエラーになってしまいます。

なので seq | while read x; do ...; done でやろうとすると、パイプの先は別プロセスなので、残念ながら動きません。
代わりに while read x; do ... ; done < <(seq) のプロセス置換でやったのはこれが理由です。

バッファリングについて

今回は awk の fflush() で挙動を確認しましたが、 stdbuf を使えばコマンドのバッファリングを切り替えられるみたいです。
https://linuxjm.osdn.jp/html/GNU_coreutils/man1/stdbuf.1.html

これだとNGだが
$ coproc hoge { tr $'\t' , ; }
こっちならOK
$ coproc hoge { stdbuf -oL  tr $'\t' , ; }

おわり。

6
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8