Condição de corrida para blocos de shell no Bash?

0

Update: This behavior is observed on Windows Subsystem for Linux. It seems there are two issues we are dealing with here:

  1. Some bug/race condition internal to the system. This is incorrect, see answers.

  2. Default buffer size for head.

For (2), as @kusalanda mentioned, head may have some default buffer size that consumes the input up to a certain point. On ArchLinux, we can see that for i < 10, we consistently see no output from tail. The same is true for Windows Subsystem for Linux (i.e. no inconsistent output for tail). For (1), it is possible that there is some bug internal to the Windows Subsystem for Linux itself that causes this race condition, as we do not observe such behavior in ArchLinux. This is incorrect, see answers. There is a "point 1", but it is different.

Estou tentando executar os seguintes comandos em bash version 4.4.19 :

{ for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }

Às vezes, vejo os resultados esperados:

$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
999
$ ~

No entanto, muitas vezes, vejo o seguinte:

$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
$ ~

Eu suspeito que esta seja uma condição de corrida. No entanto, se eu adicionar um sono no início do segundo bloco de comandos, a "condição de corrida" ainda acontece:

$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done; } | { sleep 10; head -n 1; echo ...; tail -n 1; }
0
...
$ ~

Isso é realmente uma condição de corrida? O que devo fazer para que o segundo bloco de código veja toda a entrada? Note que se eu usar 10000 ao invés de 1000 , então eu não vejo esse problema (é possível que todos esses casos tenham sorte):

$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~ { for ((i = 0; i < 10000; ++i)); do echo $i; done; } | { head -n 1; echo ...; tail -n 1; }
0
...
9999
$ ~
    
por nehcsivart 25.11.2018 / 09:20

2 respostas

1

Esta não é uma condição de corrida e não é um bug no WSL ou no ArchLinux.

Como você mencionou, é porque head está lendo mais do que "deveria" e, portanto, pode não deixar o suficiente ou qualquer coisa para que tail trabalhe. Mas não há nada no padrão ou em outro lugar que diga que head deve ler apenas uma certa quantidade de bytes; Poderia ler o arquivo inteiro e depois descartar tudo, menos a primeira linha.

Para "consertar" que em todos os casos possíveis, head teria que sempre ler sua entrada byte por byte (ou seja, fazer uma chamada de sistema para cada byte) e isso seria terrivelmente ineficiente e absolutamente inútil em 99.999 % de casos.

Se você quiser evitar isso, você pode

1) use um arquivo temporário em vez de um pipe; então

{ head -n <tmpfile; tail -n <tmpfile; }

funcionará como esperado.

2) reimplemente sua combinação cabeça / cauda com outra coisa, por exemplo. em awk :

$ seq 10000 20000 | awk -vH=2 -vT=3 '{if(NR<=H)print; else a[i++%T]=$0}END{if((j=i-T)>0)print "..."; else j=0; while(j<i)print a[j++%T]}'
10000
10001
...
19998
19999
20000
    
por 25.11.2018 / 21:28
1

Observação: se alguma informação estiver incorreta, por favor, comente para que eu possa corrigir ou excluir.

Como @mosvy e @MichaelHomer mencionados nos comentários, isso se deve ao agendador agendar cada lado do canal de maneira diferente e em momentos diferentes. Para ser claro, estamos respondendo por que o seguinte tem resultados inconsistentes:

{ for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; tail -n 1; }

com saída como:

0
...

e:

0
...
999

Dois pontos-chave estão em jogo aqui. A resposta curta é que, como a entrada no lado direito do canal nem sempre está disponível de uma só vez (ponto 1), head "consumirá" quantidades diferentes. Se toda a entrada estiver disponível (o que significa que o lado esquerdo foi concluído primeiro), toda a entrada será consumida devido à implementação de head , conforme explicado por @Kusalananda e @mosvy (ponto 2).

Primeiro mostraremos o ponto 1. A maneira mais fácil de mostrar isso é substituir tail por head :

$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
878
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
820
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } | { head -n 1; echo ...; head -n 1; }
0
...
796

Como podemos ver, a saída do segundo head é diferente a cada vez. Isso mostra que a entrada do lado esquerdo nem sempre está disponível de uma só vez (ponto 1).

Para cada caso em que há um número após ... , obteremos uma saída de 999 se usarmos tail . Para o caso em que nada veio depois de ... , veremos o mesmo para tail . Para provar isso, mostraremos o ponto 2.

Embora não haja nada que possamos realmente fazer sobre o ponto 1, nós podemos torná-lo mais estável escrevendo-o em um arquivo:

$ ~ { for ((i = 0; i < 1000; ++i)); do echo $i; done } >input

Com o arquivo, vamos lê-lo através de um pipe (veja abaixo caso de redirecionamento):

$ ~ cat input | { head -n 1; echo ...; tail -n 1; }
0
...

E, de fato, head consome tudo, não deixando nada para tail . Como tal, temos o ponto 2. Assim, com o ponto 1 e o ponto 2, podemos explicar o comportamento inconsistente:

In my version of head, at least 1000 lines will be consumed at a time if read through a pipe, and at least 1000 lines are available (the whole thing if less). If all of the left side finishes before the right side even starts, head will consume everything, leaving nothing for tail. If, however, the left side does not finish, head will only consume those that are done. This means something is leftover for tail, thus leaving an output.

Redirecionamento

Assim, no exemplo acima, usamos um pipe para fornecer o resultado. O raciocínio é que, se usássemos o redirecionamento, acabaríamos com o seguinte resultado:

$ ~ { head -n 1; echo ...; tail -n 1; } <input
0
...
999

O que é diferente da explicação acima. O raciocínio é que, quando usado dessa forma, parece que head apenas lê uma linha:

$ ~ { head -n 1; echo ...; head -n 1; } <input
0
...
1

A maneira de explicar isso é fazer referência à resposta aqui . Resumindo:

  • pipes are not lseek()'able so commands can't read some data and then rewind back, but when you redirect with > or < usually it's a file which is lseek() able object, so commands can navigate however they please.

Em outras palavras, head não precisa consumir tudo se for capaz de buscar o arquivo diretamente. Só precisa ler o quanto for necessário. Depois de encontrar uma nova linha, pode colocar tudo de volta. Podemos provar isso usando um arquivo com 1 byte após uma nova linha:

$ ~ cat input
0123456789
1
$ ~ { head -n 1; head -c 1; } <input
0123456789
1$ ~

Se usarmos um pipe, toda a entrada será consumida, sem nada para o segundo head :

$ ~ cat input | { head -n 1; head -c 1; }
0123456789
$ ~

Como uma nota secundária, se usássemos a substituição de processo (o que resulta em uma leitura não-pesquisável como eu a entendo), obteremos o mesmo resultado:

$ ~ { head -n 1; head -c 1; } < <(cat input)
0123456789
$ ~
    
por 28.11.2018 / 10:55