трубопровод с недетерминированным выходом

Я произошел по команде, которая иногда работает, а иногда и нет, даже если она выполняется несколько раз подряд в оболочке bash (я не тестировал поведение в других оболочках). Проблема была локализована для чтения переменной в блоке BEGIN оператора awk в конце линии трубопровода. Во время некоторых исполнений переменная корректно считывается в блоке BEGIN а во время других исполнений операция завершается с ошибкой. Предположим, что это аберрантное поведение может быть воспроизведено другими (и это не является следствием некоторой проблемы с моей системой), может ли быть объяснена его несогласованность?

В качестве входных данных введите следующий файл с именем tmp :

 cat > tmp <<EOF aa b * aa a aaa a aa a aa c * aaa a aaaa a d * aaa a aa aaaaa a e * aaaa a aaa a f * aa a aa g * EOF 

В моей системе линия трубопровода

  awk '{if($2!~/\*/) print $1}' tmp | tee >(wc -l | awk '{print $1}' > n.txt) | sort | uniq -c | sort -k 1,1nr | awk 'BEGIN{getline n < "n.txt"}{print $1 "\t" $1/n*100 "\t" $2}' 

будет либо производить правильный вывод:

 4 28.5714 a 4 28.5714 aaa 3 21.4286 aa 2 14.2857 aaaa 1 7.14286 aaaaa 

или сообщение об ошибке:

 awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted 

Как команда может выдавать разные результаты при запуске дважды подряд, когда не происходит генерации случайных чисел, и временное изменение среды не производится?

Чтобы продемонстрировать, насколько абсурдным является поведение, рассмотрите вывод, сгенерированный путем выполнения указанной выше линии трубопровода десять раз подряд в цикле:

 for x in {1..10}; do echo "Iteration ${x}"; awk '{if($2!~/\*/) print $1}' tmp | tee >(wc -l | awk '{print $1}' > n.txt) | sort | uniq -c | sort -k 1,1nr | awk 'BEGIN{getline n < "n.txt"}{print $1 "\t" $1/n*100 "\t" $2}'; done Iteration 1 awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted Iteration 2 4 28.5714 a 4 28.5714 aaa 3 21.4286 aa 2 14.2857 aaaa 1 7.14286 aaaaa Iteration 3 4 28.5714 a 4 28.5714 aaa 3 21.4286 aa 2 14.2857 aaaa 1 7.14286 aaaaa Iteration 4 awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted Iteration 5 awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted Iteration 6 awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted Iteration 7 4 28.5714 a 4 28.5714 aaa 3 21.4286 aa 2 14.2857 aaaa 1 7.14286 aaaaa Iteration 8 awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted Iteration 9 4 28.5714 a 4 28.5714 aaa 3 21.4286 aa 2 14.2857 aaaa 1 7.14286 aaaaa Iteration 10 awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted 

Примечание. Я также попытался закрыть файл (awk close ) после чтения переменной, если проблема связана с тем, что файл остается открытым. Однако непоследовательный выход остается.

Ваши переадресации имеют условие гонки. Эта:

 >(wc -l | awk '{print $1}' > n.txt) 

работает параллельно с:

 awk 'BEGIN{getline n < "n.txt"}...' 

позже в трубопроводе. Иногда n.txt по-прежнему пуст, когда программа awk запускается.

Это (наклонно) описано в Справочном руководстве Bash. В трубопроводе :

Выход каждой команды в конвейере подключается через трубу к входу следующей команды. То есть каждая команда считывает вывод предыдущей команды. Это соединение выполняется перед любыми перенаправлениями, указанными в команде .

а потом:

Каждая команда в конвейере выполняется в своей собственной подоболочке

(выделено курсивом). Все процессы в конвейере запускаются, причем их вход и выход соединены вместе, не дожидаясь завершения каких-либо из предыдущих программ или даже начать что-либо делать. До этого подстановка процесса с помощью >(...) :

выполняются одновременно с расширением параметра и переменной, подстановкой команд и арифметическим расширением.

Это означает, что подпроцесс, выполняющий wc -l | awk ... Команда wc -l | awk ... запускается на ранней стадии, а перенаправление опустошает n.txt непосредственно перед этим, но процесс awk , вызывающий ошибку, запускается вскоре после этого. Обе эти команды выполняются параллельно – у вас будет сразу несколько процессов.

Ошибка возникает, когда awk запускает свой блок BEGIN до того, как вывод команды wc был записан в n.txt . В этом случае n переменная пуста и поэтому равна нулю при использовании в качестве числа. Если BEGIN запускается после заполнения файла, все работает.

Когда это происходит, зависит от планировщика операционной системы, и какой процесс сначала получает слот, что по сути является случайным с точки зрения пользователя. Если последний awk запускается рано или конвейер wc получает запланированное несколько позже, файл все равно будет пустым, когда awk начнет выполнять свою работу, и все это сломается. По всей вероятности, процессы будут выполняться на разных ядрах на самом деле одновременно, и это до тех пор, пока в первую очередь не дойдет до утверждения. Эффект, который вы получите, вероятно, чаще всего работает с командой, но иногда с ошибкой вы публикуете.


В общем, конвейеры являются безопасными только в том случае, если они всего лишь конвейеры – стандартный вывод в стандартный ввод прекрасен, но поскольку процессы выполняются параллельно , нецелесообразно полагаться на последовательность любых других каналов связи , например файлов или любой части любого процесса, выполняемого до или после любой части другой, если они не заблокированы вместе, читая стандартный ввод.

Обходной путь здесь, вероятно, заключается в том, чтобы делать все ваши записи файлов до их необходимости: в конце строки гарантируется, что весь конвейер и все его перенаправления завершились до запуска следующей команды. Эта команда никогда не будет надежной, но если вам действительно нужно, чтобы она работала в такой структуре, вы можете вставить задержку ( sleep ) или цикл до n.txt пор, пока n.txt не будет пустым перед запуском последней команды awk чтобы увеличить шансы о том, как вы хотите.

выражение pipe в process substitution вызывает состояние расы в bash и ksh , zsh – нет.

Основная проблема здесь в том, что zsh ждет, bash этого не делает.

Здесь вы можете увидеть более подробную информацию.

Быстрое исправление, добавление sleep 1 в ваш awk чтобы сделать n.txt всегда доступным:

 awk 'BEGIN{system("sleep 1");getline n < "n.txt"};{print $1 "\t" $1/n*100 "\t" $2}' 

Состояние гонки уже определено. Но если вам нужно упрощенное решение, вам не нужен отдельный wc для подсчета записей, awk может это сделать:

 awk '{if($2!~/\*/){print $1;++n}END{print n >"n.txt"}' tmp | sort | uniq -c ... 

Кроме того, awk может рассчитывать как sort|uniq -c до тех пор, пока значения соответствуют памяти, а также вычисляют x / n, но могут выводиться в «случайном» порядке; также использование match / action более аккуратное:

 awk '$2!~/\*/{++k[$1];++n} END{for(i in k){print k[i]"\t"k[i]/n*100"\t"i}}' tmp | sort -k1nr 

Или в недавнем GNU awk вы можете установить PROCINFO["sorted_in"]="@ind_num_desc" поэтому for использует правильный порядок, и вам не нужен этот sort .