我想看到twice
输出两次,但这个脚本只会输出一次:
dump() {
(sleep 1; cat) > "$1"
}
(sleep 0; echo "twice") | tee >(dump "./a.txt")
echo "$(< "a.txt")"
为了看两次,我必须调整睡眠时间:
dump() {
(sleep 0; cat) > "$1"
}
(sleep 1; echo "twice") | tee >(dump "./a.txt")
echo "$(< "a.txt")"
是什么导致了这里的竞争条件?
答案1
进程替换中的调用dump
作为异步进程运行。这意味着将tee
其输出写入其中,然后管道完成。管道完成是因为输出tee
被缓冲;如果你写了数据量多于管道缓冲区的大小,tee
必须等待dump
使用它,并且您的原始代码很可能会工作。
假设您只写入少量数据,就像在问题中所做的那样,然后您会a.txt
在管道终止后立即读取数据,然后才有dump
机会将任何内容写入文件(它仍在后台休眠,数据挂起)在管道缓冲区周围)。
如果您在运行错误代码后查看该a.txt
文件,您会注意到它包含字符串twice
。因此,sleep 1
在函数中提供的轻微延迟之后,它最终确实到达了那里。
要阻止管道过早终止,请cat
在末尾添加:
dump() {
(sleep 1; cat) > "$1"
}
(sleep 0; echo "twice") | tee >(dump "./a.txt") | cat
echo "$(< "a.txt")"
这使得它可以工作,因为现在该cat
进程需要等待 的输出dump
通过管道到达(不会有,但它不知道)。这会延迟管道的终止,直到dump
调用返回。此时,数据已被写入,a.txt
并且可以通过脚本中的最后一个命令来获取。
管道中唯一同步进程的是 I/O,即从前一个进程读取数据并写入下一个进程。如果一个进程希望在某个时刻从上一步读取数据,它将阻塞,直到有数据可供读取,或者直到上一步关闭了管道的末端。
cat
默认从标准输入读取。添加的标准输入cat
连接到管道上一步中的标准输出tee
和过程替换。dump
该cat
实用程序将一直读取,直到没有其他内容可读取为止。直到tee
和都dump
完成执行后才会发生这种情况。
代码的清理版本:
dump() {
sleep 1
cat >"$1"
}
echo twice | tee >(dump ./a.txt) | cat
cat a.txt
答案2
(sleep 0; echo "twice") | tee >(dump "./a.txt")
echo "$(< "a.txt")"
IIUC,问题是如何等待里面的进程>(...)
完成后再执行$(...)
下一行的命令替换。
答案是没有好的方法可以做到这一点。如果您的系统支持该/dev/fd/
机制,您可以使用一个exec fd> >(...)
技巧:
dump() {
(sleep 1; cat) > "$1"
}
echo twice | { exec 7> >(dump a.txt); tee /dev/fd/7; exec 7>&-; wait; }
echo "$(< "a.txt")"
是的,这很丑陋,但你可以做得更糟:
echo twice | { a=>(dump a.txt); tee "$a"; eval "exec ${a##*/}>&-"; wait; }
因为它可以为此收集,A)使用较新版本的 bash (>= 5.0),您可以在wait
内部运行进程>(...)
b)这些进程可能不会终止,直到它们在其标准输入上获得 EOF,直到您关闭管道的另一端(/dev/fd/63
或类似的>(...)
已扩展到的管道),这才会发生。后者很难正确执行。
答案3
某些版本的 Bash 有一个错误,导致它们无法正确等待进程替换生成的进程。