Bash의 쉘 블록에 대한 경쟁 조건이 있습니까?

Bash의 쉘 블록에 대한 경쟁 조건이 있습니까?

업데이트: 이 동작은 Linux용 Windows 하위 시스템에서 관찰되었습니다. 여기서는 두 가지 문제를 다루고 있는 것 같습니다.

  1. 시스템 내의 일부 오류/경합 상태. 이것은 틀렸습니다. 답변을 참조하세요.

  2. 기본 버퍼 크기입니다 head.

(2)의 경우 @kusalanda가 언급했듯이 head특정 지점까지 입력을 소비하는 기본 버퍼 크기가 있을 수 있습니다. ArchLinux 에서는 i < 10. tailLinux용 Windows 하위 시스템의 경우에도 마찬가지입니다(즉, 일관되지 않은 출력 없음 tail). (1)과 관련하여 Linux용 Windows 하위 시스템 자체에 이러한 경쟁 조건을 일으키는 일부 내부 버그가 있을 수 있습니다. ArchLinux에서는 이 동작을 관찰하지 못했기 때문입니다. 이것은 틀렸습니다. 답변을 참조하세요. "포인트 1"이 있지만 다릅니다.

bash버전에서 다음 명령을 실행하려고합니다 4.4.19.

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

때로는 예상되는 결과가 표시됩니다.

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

그러나 다음과 같은 경우가 종종 있습니다.

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

나는 이것이 경쟁 조건이라고 생각합니다. 그러나 두 번째 명령 블록 시작 부분에 절전 모드를 추가하면 "경합 조건"이 계속 발생합니다.

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

이것이 실제로 경쟁 조건입니까? 두 번째 코드 블록에서 전체 입력을 보려면 어떻게 해야 합니까? 10000대신 을 사용하면 1000이 문제가 발생하지 않습니다(단지 운이 좋은 상황일 수도 있음).

$ ~ { 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
$ ~

답변1

이것은 경쟁 조건이 아니며오류 없음WSL 또는 ArchLinux에서.

당신이 언급한 것처럼, 이는 head"해야 하는" 것 이상을 읽으면 작업할 내용이 충분하지 않거나 전혀 남지 않기 때문입니다 tail. 그러나 표준이나 다른 곳에서는 head특정 수의 바이트만 읽어야 한다는 내용이 없습니다 . 전체 파일을 읽고 첫 번째 줄을 제외한 모든 내용을 삭제할 수도 있습니다.

가능한 모든 경우에 이 문제를 "수정"하려면 head항상 입력을 바이트 단위로 읽어야 합니다(즉, 각 바이트에 대해 시스템 호출을 수행해야 합니다). 이는 매우 비효율적이며 99.999%의 경우 쓸모가 없는 경우 절대 불가능합니다.

이것을 피하고 싶다면 할 수 있습니다

1) 파이프 대신 임시 파일을 사용하십시오.

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

예상대로 작동합니다.

2) 머리/꼬리 조합을 다른 것으로 다시 구현하십시오. 존재하다 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

답변2

참고: 정보 오류가 있을 경우 댓글로 남겨주시면 수정 또는 삭제할 수 있도록 하겠습니다.

@mosvy와 @MichaelHomer가 댓글에서 언급했듯이 이는 스케줄러가 파이프라인의 각 측면을 서로 다른 시간에 다르게 예약하기 때문입니다. 명확하게 하기 위해 다음 출력이 일관되지 않은 이유에 대해 답하고 있습니다.

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

출력은 다음과 같습니다.

0
...

그리고:

0
...
999

여기에는 두 가지 핵심 사항이 있습니다. 즉, 파이프 오른쪽의 입력이 항상 한꺼번에 사용 가능한 것은 아니기 때문에(포인트 1) head서로 다른 양이 "소비"됩니다. 전체 입력을 사용할 수 있는 경우(왼쪽이 먼저 완료됨을 의미) head@Kusalananda 및 @mosvy(포인트 2)에서 설명하는 구현으로 인해 전체 입력이 소비됩니다.

먼저 포인트 1을 보여드리겠습니다. 이를 시연하는 가장 쉬운 방법은 다음 tail으로 바꾸는 것입니다 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

보시다시피 두 번째 출력 head은 매번 다릅니다. 이는 왼쪽 입력이 항상 동시에 사용 가능한 것은 아니라는 것을 보여줍니다(점 1).

...숫자가 이어지는 모든 경우에 대해 if의 출력을 얻습니다 . 아무것도 나오지 않는 이후의 경우에도 동일한 내용을 보게 될 것입니다. 이를 증명하기 위해 포인트 2를 보여드리겠습니다.999tail...tail

첫 번째 항목에 대해서는 우리가 할 수 있는 일이 없지만,할 수 있는파일에 기록하여 더욱 안정적으로 만드세요.

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

이 파일의 경우 파이프를 통해 읽습니다(아래 리디렉션 사례 참조).

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

사실 head모든 것이 소모되고 아무것도 남지 않습니다 tail. 따라서 포인트 2가 있습니다. 따라서 포인트 1과 2를 통해 일관성 없는 동작을 설명할 수 있습니다.

내 버전에서는 head파이프를 통해 읽는 경우 한 번에 최소 1000개의 행이 소비되고 최소 1000개의 행을 사용할 수 있습니다(적을 경우 모두). 오른쪽이 시작되기 전에 왼쪽의 모든 것이 끝나면 head모든 것이 소모되어 아무것도 남지 않게 됩니다 tail. 단, 왼쪽 부분이 완성되지 않은 경우 head완성된 부분만 소모됩니다. 이는 무언가가 뒤에 남겨져 tail결과가 뒤에 남는다는 것을 의미합니다.

리디렉션

따라서 위의 예에서는 파이프를 사용하여 결과를 제공합니다. 그 이유는 리디렉션을 사용하면 다음과 같은 결과가 발생하기 때문입니다.

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

위의 설명과 다릅니다. 그 이유는 이 방법을 사용하면 head1개의 행만 읽는 것처럼 보이기 때문입니다.

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

이 질문을 설명하는 방법은 답변을 인용하는 것입니다.여기. 간단히 말해서:

  • 파이프는 lseek()를 지원하지 않으므로 명령이 일부 데이터를 읽은 다음 되감을 수 없습니다. 그러나 > 또는 <로 리디렉션하면 일반적으로 lseek()를 지원하는 개체 파일이므로 명령이 다음 위치에서 탐색할 수 있습니다. 할 것이다.

즉, head파일을 직접 찾을 수 있다면 모든 것을 소비할 필요가 없습니다. 필요한 내용만 읽으면 됩니다. 개행 문자를 찾으면 모든 것을 다시 되돌릴 수 있습니다. 개행 문자 뒤에 1바이트가 있는 파일을 사용하여 이를 증명할 수 있습니다.

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

파이프를 사용하면 전체 입력이 소비되고 두 번째 입력에는 아무것도 남지 않습니다 head.

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

참고로 프로세스 대체를 사용하면(내가 아는 한 검색할 수 없는 읽기가 발생함) 동일한 결과를 얻습니다.

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

관련 정보