Сопроцессы: -что, -как, -зачем?

Многие пользователи Bash знают о существании со-процессов, появившихся в 4-й версии Bash’a. Несколько меньшее количество знает о том, что сопроцессы в Bash не какая-то новая фича, а древний функционал KornShell’a появившийся ещё в реализации ksh88 в 1988 году. Ещё меньшее количество пользователей shell’ов умеющих сопроцессить знают синтаксис и помнят как это делать.
Вероятно, я отношусь к четвёртой группе — знающих о сопроцессах, периодически умеющих ими пользоваться, но так и не понимающих «зачем?». Я говорю «периодически», так как иногда я освежаю в голове их синтаксис, но к тому моменту, когда мне кажется что «вот тот случай когда можно применить co-proc» я уже напрочь забываю о том как это делать.
Этой заметкой я хочу свести воедино синтаксисы для разных шеллов чтобы на случай, если таки придумаю зачем они мне нужны, я если и не вспомню как это делать, то по крайней мере, буду знать где это записано.
В заголовке статьи у нас 3 вопроса. Пойдём по порядку.

Что?
Что же такое co-process?
Со-процессинг — это одновременное выполнение двух процедур, одна из которых считывает вывод другой. Для его реализации необходимо предварительно запустить фоновый процесс выполняющий функционал канала. При запуске фонового процесса его stdin и stdout присваиваются каналам связанными с пользовательскими процессами. Соответственно, один канал для записи, второй для чтения.
Пояснять это проще на примерах, поэтому сразу перейдём ко второму вопросу.

Как?
Реализации со-процессов в шеллах разнятся. Я остановлюсь на 3-х известных мне реализациях в ksh, zsh и bash. Рассмотрим их в хронологическом порядке.
Хоть это и не имеет прямого отношения к вопросам статьи, отмечу, что все нижеприведённые примеры сделаны на

$ uname -opr
FreeBSD 10.1-STABLE amd64

Ksh
$ `echo $0` --version
  version         sh (AT&T Research) 93u+ 2012-08-01

Синтаксис
cmd |&

кажется мне наиболее логичным. Здесь для выполнения команды cmd в фоновом режиме мы используем специальную операцию |&, выражающую соответвенно:
 — »&» — фоновый процесс;
 — »|» — каналы.

Запускаем фоновый процесс:

$ tr -u a b |&
[2]     6053

Убедимся, что он жив:
$  ps afx | grep [6]053
 6053  4  IN        0:00.00 tr -u a b

Теперь мы можем общаться с нашим фоновым процессом.
Пишем:
$ print -p abrakadabra1
$ print -p abrakadabra2
$ print -p abrakadabra3

и читаем:
$ read -p var; echo $var
bbrbkbdbbrb1
$ read -p var; echo $var
bbrbkbdbbrb2
$ read -p var; echo $var
bbrbkbdbbrb3

или так:
$ print abrakadabra1 >&p
$ print abrakadabra2 >&p
$ print abrakadabra3 >&p
$ while read -p var; do echo $var; done
bbrbkbdbbrb1
bbrbkbdbbrb2
bbrbkbdbbrb3

Закрываем «конец» трубы для записи:
$ exec 3>&p 3>&-

и для чтения:
$ exec 3<&p 3<&-

Zsh
$ `echo $0` --version
zsh 5.2 (amd64-portbld-freebsd10.1)

Синтаксис со-процессов в zsh не слишком отличается от ksh, что не удивительно, т.к. в его man’е сказано «zsh most closely resembles ksh».
Основным отличием является использование ключевого слова coproc вместо оператора |&. В остальном всё очень похоже:
$ coproc tr -u a b
[1] 22810
$ print -p abrakadabra1
$ print abrakadabra2 >&p
$ print -p abrakadabra3
$ read -ep
bbrbkbdbbrb1
$ while read -p var; do echo $var; done
bbrbkbdbbrb2
bbrbkbdbbrb3

Для закрытия каналов чтения/записи можно воспользоваться идиомой exit:
$ coproc exit
[1] 23240
$
[2]  - done       tr -u a b
$
[1]  + done       exit

При этом запустился новый фоновый процесс, который тут же завершился.
Это ещё одно отличие от ksh — мы можем не закрывать существующий сопроцесс, а сразу инициировать новый:
$ coproc tr -u a b
[1] 24981
$ print -p aaaaa
$ read -ep
bbbbb
$ coproc tr -u a d
[2] 24982
$
[1]  - done       tr -u a b
$ print -p aaaaa
$ read -ep
ddddd
$

в ksh мы бы просто получили:
$ tr -u a b |&
[1]     25072
$ tr -u a d |&
ksh93: process already exists

Несмотря на эту возможность рекомендуется, всегда явно убивать фоновый процесс, особенно, при использовании «setopt NO_HUP».
Здесь же стоит упомянуть, что иногда мы можем получить неожиданные результаты связанные с буферизацией вывода, именно поэтому в приведённых выше примерах мы используем tr с опцией -u.
$ man tr | col | grep "\-u"
     -u      Guarantee that any output is unbuffered.

Хоть это и не имеет оношения исключительно к со-процессам продемонстрирую это поведение примером:
$ coproc tr a b
[1] 26257
$ print -p a
$ read -ep
^C
$
[1]  + broken pipe  tr a b

Буфер не полон и мы ничего не получаем из нашей трубы. Заполним его «доверху»:
$ coproc tr a b
[1] 26140
$ for ((a=1; a <= 4096 ; a++)) do print -p 'a'; done
$ read -ep
b

Разумеется, если данное поведение нас не устраивает, его можно изменить, например используя stdbuf
$ coproc stdbuf -oL -i0 tr a b
[1] 30001
$ print -p a
$ read -ep
b

Bash
$ `echo $0` --version
GNU bash, version 4.3.42(1)-release (amd64-portbld-freebsd10.1)

Для запуска со-процесса в bash также как и в zsh используется зарезервированное слово coproc, но в отличии от рассмотренных выше shell’ов доступ к сопроцессу осуществляется не с помощью >&p и <&p, а посредством массива $COPROC:
 — ${COPROC[0]} для записи;
 — ${COPROC[1]} для чтения.
Соответственно, процедура записи/чтения будет выглядеть примерно так:
$  coproc tr -u a b
[1] 30131
$ echo abrakadabra1 >&${COPROC[1]}
$ echo abrakadabra2 >&${COPROC[1]}
$ echo abrakadabra3 >&${COPROC[1]}
$ while read -u ${COPROC[0]}; do printf "%s\n" "$REPLY"; done
bbrbkbdbbrb1
bbrbkbdbbrb2
bbrbkbdbbrb3

, а закрытие дескрипторов:
$ exec {COPROC[1]}>&-
$ cat <&"${COPROC[0]}"
[1]+  Done                    coproc COPROC tr -u a b

Если имя COPROC по каким-то причинам не устраивает можно указать свое:
$ coproc MYNAME (tr -u a b)
[1] 30528
$ echo abrakadabra1 >&${MYNAME[1]}
$ read -u ${MYNAME[0]} ; echo $REPLY
bbrbkbdbbrb1
$  exec {MYNAME[1]}>&- ; cat <&"${MYNAME[0]}"
[1]+  Done                    coproc MYNAME ( tr -u a b )

Зачем?
Прежде чем попытаться ответить зачем нужны сопроцессы подумаем можно ли реализовать их функционал в shell’ах которые не имеют coproc «из коробки». Например в таком:
$ man sh | col -b | grep -A 4 DESCRIPTION
DESCRIPTION
     The sh utility is the standard command interpreter for the system.  The
     current version of sh is close to the IEEE Std 1003.1 (``POSIX.1'') spec-
     ification for the shell.  It only supports features designated by POSIX,
     plus a few Berkeley extensions.
$ man sh | col -b | grep -A 1 -B 3 AUTHORS
     This version of sh was rewritten in 1989 under the BSD license after the
     Bourne shell from AT&T System V Release 4 UNIX.

AUTHORS
     This version of sh was originally written by Kenneth Almquist.

Именованные каналы никто не отменял:
$ mkfifo in out
$ tr -u a b out &
$ exec 3> in 4< out
$ echo abrakadabra1 >&3
$ echo abrakadabra2 >&3
$ echo abrakadabra3 >&3
$ read var <&4 ; echo $var
bbrbkbdbbrb1
$ read var <&4 ; echo $var
bbrbkbdbbrb2
$ read var <&4 ; echo $var
bbrbkbdbbrb3

Пожалуй, с некоторой натяжкой, можно сказать, что это же реализуемо с помощью псевдотерминалов, но развивать эту тему не стану.

Ну и зачем же нужны сопроцессы? Я процитирую выдержку из перевода статьи Mitch Frazier:

Пока я не могу придумать никаких <...> задач для со-процессов, по крайней мере не являющихся надуманными.

И в действительности я лишь один раз смог с относительной пользой применить со-процессы в своих скриптах. Задумка была реализовать некий «persistent connect» для доступа к MySQL.
Выглядело это примерно так:
$ coproc stdbuf -oL -i0 mysql -pPASS
[1] 19743
$ printf '%s;\n' 'select NOW()' >&${COPROC[1]}
$ while read -u ${COPROC[0]}; do printf "%s\n" "$REPLY"; done
NOW()
2016-04-06 13:29:57

В остальном все мои попытки использовать coproc действительно были надуманными.
Спасибо
Хочется поблагодарить Bart Schaefer, Stéphane Chazelas, Mitch Frazier чьи комментарии, письма и заметки помогли в написании статьи.

Комментарии (0)

© Habrahabr.ru