pipe line com saída não determinística

4

Eu deparei com um comando que às vezes funciona e às vezes não, mesmo quando executado várias vezes em rápida sucessão em um bash shell (não testei o comportamento em outros shells). O problema foi localizado para a leitura de uma variável no bloco BEGIN de uma instrução awk no final da linha de tubulação. Durante algumas execuções, a variável é lida corretamente no bloco BEGIN e durante outras execuções, a operação falha. Supondo que este comportamento aberrante possa ser reproduzido por outros (e não é uma consequência de algum problema com o meu sistema), a sua inconsistência pode ser explicada?

Tome como entrada o seguinte arquivo chamado tmp :

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

No meu sistema, a linha de tubulação

 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}'

produzirá a saída correta:

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

ou a mensagem de erro:

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

Como pode um comando possivelmente dar saída diferente quando executado duas vezes em sucessão quando nenhuma geração de número aleatório está envolvida e nenhuma alteração no ambiente é feita no ínterim?

Para demonstrar o quão absurdo é o comportamento, considere a saída gerada pela execução da linha de tubulação acima dez vezes consecutivamente em um loop:

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

Nota: Eu também tentei fechar o arquivo (awk close ) depois de ler a variável, caso o problema esteja relacionado ao arquivo que está sendo deixado aberto. No entanto, a saída inconsistente permanece.

    
por user001 11.08.2014 / 03:18

3 respostas

13

Seus redirecionamentos têm uma condição de corrida. Isso:

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

é executado em paralelo com:

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

mais tarde no pipeline. Às vezes, n.txt ainda está vazio quando o programa awk começa a ser executado.

Isso é documentado (obliquamente) no Manual de Referência do Bash. Em um canal :

The output of each command in the pipeline is connected via a pipe to the input of the next command. That is, each command reads the previous command’s output. This connection is performed before any redirections specified by the command.

e depois:

Each command in a pipeline is executed in its own subshell

(ênfase adicionada). Todos os processos no pipeline são iniciados, com suas entradas e saídas conectadas juntas, sem esperar que algum dos programas anteriores conclua ou mesmo comece a fazer qualquer coisa. Antes disso, processo de substituição com >(...) is:

performed simultaneously with parameter and variable expansion, command substitution, and arithmetic expansion.

O que isso significa é que o subprocesso executando o comando wc -l | awk ... inicia no início e o redirecionamento esvazia n.txt logo antes disso, mas o processo awk que causa o erro é iniciado logo após. Ambos os comandos são executados em paralelo - você terá vários processos ao mesmo tempo aqui.

O erro ocorre quando awk executa seu bloco BEGIN antes que a saída do comando wc tenha sido gravada em n.txt . Nesse caso, a variável n está vazia e, portanto, é zero quando usada como um número. Se o BEGIN for executado após o preenchimento do arquivo, tudo funcionará.

Quando isso acontece, depende do agendador do sistema operacional e de qual processo recebe um slot primeiro, que é essencialmente aleatório da perspectiva do usuário. Se o awk final for executado antes ou o wc pipeline for agendado um pouco mais tarde, o arquivo ainda estará vazio quando awk começar a fazer seu trabalho e a coisa toda será interrompida. Com toda a probabilidade, os processos serão executados em diferentes núcleos, na verdade, simultaneamente, e é para baixo que se chega ao ponto de contenção primeiro. O efeito que você terá é provavelmente o comando trabalhando com mais freqüência do que não, mas às vezes falhando com o erro que você postou.

Em geral, os pipelines são seguros apenas na medida em que são apenas pipelines - a saída padrão na entrada padrão é boa, mas como os processos são executados em paralelo não é confiável confiar no sequenciamento de qualquer outra comunicação canais , como arquivos, ou qualquer parte de qualquer processo em execução antes ou depois de qualquer parte de outra, a menos que eles estejam bloqueados lendo a entrada padrão.

A solução aqui é provavelmente fazer todos os seus arquivos antes de precisar deles: no final de uma linha, é garantido que um pipeline inteiro e todos os seus redirecionamentos tenham sido concluídos antes do próximo comando ser executado. Este comando nunca será confiável, mas se você realmente precisar dele para trabalhar neste tipo de estrutura, você pode inserir um atraso ( sleep ) ou fazer um loop até que n.txt esteja não vazio antes de executar o comando awk final. para aumentar as chances de as coisas funcionarem como você quer.

    
por 11.08.2014 / 04:08
5
A expressão

pipe em process substitution causa uma condição de corrida em bash e ksh , zsh não.

O principal problema aqui é que zsh aguarda, bash não.

Você pode ver mais detalhes aqui .

Uma correção rápida, adicionando sleep 1 em seu awk para tornar n.txt sempre disponível:

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

A condição de corrida já está identificada. Mas se você quiser uma solução mais fácil, não precisa de um wc separado para contar os registros, awk pode fazer isso:

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

Além disso, awk pode contar como sort|uniq -c desde que os valores caibam na memória, e também fazer o cálculo x / n, mas pode resultar em ordem "aleatória"; também usando correspondência / ação é mais arrumado:

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

Ou no recente GNU awk , você pode definir PROCINFO["sorted_in"]="@ind_num_desc" , então o for usa a ordem correta e você não precisa do sort .

    
por 11.08.2014 / 16:05