Сопроцессы: -що, -як-навіщо?

Багато користувачів 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 DESCRIPTION 4
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 <in >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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.