如果我运行

$#/bin/bash
for i in `seq 5`; do
    exec 3> >(sed -e "s/^/$i: /"; echo "$i-")
    echo foo >&3
    echo bar >&3
    exec 3>&-
done


,则结果不同步;可能是这样的:

1: foo
1: bar
2: foo
2: bar
1-
3: foo
3: bar
2-
3-
4: foo
5: foo
4: bar
5: bar
4-
5-


如何确保在进行下一个迭代之前完成过程替换>(...)

插入sleep 0.1之后的exec 3>&-有所帮助,但是它不雅致,效率低下,并且不能保证始终正常工作。我正在做的是读取循环中的输入流,将每一行馈入一个在循环中偶尔更改的进程。在代码中更容易解释:

# again, simplified for illustration
while IFS= read line; do
    case $line in
    @*)
        exec 3>&-
        filename=${line:1}
        echo "starting $filename"
        exec 3> >(sort >"$filename"; echo "finished $filename")
        ;;
    *)
        echo "$line" >&3
        ;;
    esac
done
exec 3>&-


评论

与问题无关,但seq是错误的形式(并非在所有平台上都可用,支持bash,效率相对较低);考虑使用C样式的for循环:for((i = 0; i <5; i ++))

至于眼前的问题,最好的答案是“不要那样做”。如果您可以说明要达到的目标,我们可以提供一种更好的方法来实现该目标。

有一个相关的常见问题解答项目。

#1 楼

以下是在bash 4中使用协同处理的工作:

#!/bin/bash
fd_re='^[0-9]+$'
cleanup_and_wait() {
    if [[ ${COPROC[1]} =~ $fd_re ]] ; then
        eval "exec ${COPROC[1]}<&-"
        echo "waiting for $filename to finish" >&2
        wait $COPROC_PID
    fi
}

while IFS= read -r line; do
    case $line in
    @*)
        cleanup_and_wait
        filename=${line:1}
        echo "starting $filename" >&2
        coproc { sort >"$filename"; echo "Finished with $filename" >&2; }
        ;;
    *)
        printf '%s\n' "$line" >&${COPROC[1]}
        ;;
    esac
done
cleanup_and_wait


对于以前的bash版本,可以使用命名管道代替:

cleanup_and_wait() {
    if [[ $child_pid ]] ; then
      exec 4<&-
      echo "waiting for $filename to finish" >&2
      wait $child_pid
    fi
}

# this is a bit racy; without a force option to mkfifo,
# however, the race is unavoidable
fifo_name=$(mktemp -u -t fifo.XXXXXX)
if ! mkfifo "$fifo_name" ; then
  echo "Someone else may have created our temporary FIFO before we did!" >&2
  echo "This can indicate an attempt to exploit a race condition as a" >&2
  echo "security vulnarability and should always be tested for." >&2
  exit 1
fi

# ensure that we clean up even on unexpected exits
trap 'rm -f "$fifo_name"' EXIT

while IFS= read -r line; do
    case $line in
    @*)
        cleanup_and_wait
        filename=${line:1}
        echo "starting $filename" >&2
        { sort >"$filename"; echo "finished with $filename" >&2; } <"$fifo_name" &
        child_pid=$!
        exec 4>"$fifo_name"
        ;;
    *)
        printf '%s\n' "$line" >&4
        ;;
    esac
done
cleanup_and_wait


一些注意事项:


使用printf '%s\n' "$line"echo "$line"更安全;例如,如果一行仅包含-e,则某些版本的echo对此将不起作用。
使用EXIT陷阱进行清理可确保意外的SIGTERM或其他错误不会使过时的fifo呆在身边。 />如果您的平台提供了一种通过单个原子操作创建具有未知名称的FIFO的方法,请使用该方法;这样可以避免出现要求我们始终测试mkfifo是否成功的情况。


评论


不知道为什么,但是如果直接使用> fifo,而不使用文件描述符(例如3),则子进程将在第一个命令printf处停止。我回答的目的是避免最大程度地更改代码。

– Nahuel Fouilleul
2012年6月21日15:03



@NahuelFouilleul好的,谢谢,我写的方式是试图多次打开FIFO的输入端。全面测试了协同处理版本;我应该对FIFO版本执行相同的操作。

–查尔斯·达菲(Charles Duffy)
2012年6月21日15:43

您还需要关闭文件描述符4>&-

– Nahuel Fouilleul
2012年6月21日16:00

@NahuelFouilleul我已经作为cleanup_and_wait函数的一部分进行了此操作。

–查尔斯·达菲(Charles Duffy)
2012年6月21日19:17

一个警告:如果您执行some_cmd |而IFS =读取行;如果执行...,则while循环在子shell中运行,并且在循环之后将无法识别它启动的协同进程。您应该在IFS =读取行时进行写入;做...完成<<(some_cmd);一个常见的把戏,以及另一种使用流程替代的方法!

–音乐爱好者
2012年6月21日19:40

#2 楼

轻松,只需将所有内容通过管道传送到cat。

#!/bin/bash
for i in `seq 5`; do
  {
  exec 3> >(sed -e "s/^/$i: /"; echo "$i-")
  echo foo >&3
  echo bar >&3
  exec 3<&-
  }|cat
done


这里的输出是:

1: foo
1: bar
1-
2: foo
2: bar
2-
3: foo
3: bar
3-
4: foo
4: bar
4-
5: foo
5: bar
5-


评论


这是一个绝妙的把戏!但是,在我的第二个示例中(在“ EDIT”下),打开文件描述符,对其进行写入和关闭该过程并未顺序放置在代码中,以允许将其包装在列表中,这使得很难应用您的方法。

–音乐爱好者
2015年6月12日19:15在

我尝试了您的“ EDIT”代码,每行分别输入100个文件名行(@ 00- @ 99),然后输入100行对应编号的数字,再加上一个计数器。在bash 3.2下,输出是干净的(文件23依次包含100行“ 23-00”至“ 23-99”等)。

–ruief
15年6月12日在22:11

标准输出怎么样:“完成n”后总是“开始n + 1”?您是否可以将您的方法应用于代码?

–音乐爱好者
2015年6月13日在0:04



我无法将这种方法应用于订购“完成”的输出。您可能会发现此答案很有帮助:unix.stackexchange.com/a/388530/32078

–ruief
18年7月11日在20:34

#3 楼

mkfifo tmpfifo
for i in `seq 5`; do
  { sed -e "s/^/$i: /"; echo "$i-";} <tmpfifo &
  PID=$!
  exec 3> tmpfifo
  echo foo >&3
  echo bar >&3
  exec 3>&-
  wait $PID
done
rm tmpfifo


评论


这是一个好方法。作为第二个(“如果您不使用bash 4 ...”的话),我采用了一些健壮性修复程序并对其进行了改进。

–查尔斯·达菲(Charles Duffy)
2012年6月21日14:53

#4 楼

“显而易见”的答案是摆脱流程替换。

for i in `seq 5`; do
    echo foo | sed -e "s/^/$i: /"; echo "$i-"
    echo bar | sed -e "s/^/$i: /"; echo "$i-"
done


所以问题就变成了,您真的需要使用流程替换来构造代码吗?上面的内容比尝试同步异步构造要简单得多。

评论


好吧-使用两个sed; echo结构而不是类似{echo foo;回声条} | {sed;回声; }或{sed; echo;} <<(echo foo; echo bar)(如果希望在处理过程中设置变量)并不完全忠于原始形式提供的某些效率...但是,是的,问题在手的确是为什么?!

–查尔斯·达菲(Charles Duffy)
2012年6月21日在1:35



好一点,它只是原始文档中的一个sed过程,它分两个阶段接收其输入。

–chepner
2012年6月21日在1:40

是的,在我的示例中,sed仅用于说明目的,我不能仅仅像上面的状态那样替换流程替换。我需要将循环中各个迭代的输出馈入单个进程替换中。请参阅以上文章中的我的编辑。

–音乐爱好者
2012年6月21日,2:15

顺便说一句-seq是非标准命令(在POSIX中找不到)。如果为bash编写,则使用C样式的for循环更安全:for((i = 0; i <5; i ++))

–查尔斯·达菲(Charles Duffy)
13年3月27日在13:19

#5 楼

另一个用户问相同的问题,并在此处收到详尽的答案。