即使遇到“等待”也要进入比赛状态

时间:2018-07-12 01:26:02

标签: bash wait race-condition flush diskcache

我在bash程序中遇到了奇怪的比赛情况。我尝试通过一个足够简单的演示程序来复制它,但是显然,对于所有/大多数与计时相关的比赛演示尝试,都是如此。

这是程序的抽象版本,不重复该问题,但让我仍然说明:

# Abstracted version of the original program
# that is NOT able to demo the race.
#
function foo() {
    local instance=$1

    # [A lot of logic here -
    #  all foreground commands, nothing in the background.]

    echo "$instance: test" > /tmp/foo.$instance.log        
    echo "Instance $instance ended"
}

# Launch the process in background...
#
echo "Launching instance 1"
foo 1 &

# ... and wait for it to complete.
#
echo "Waiting..."
wait
echo "Waiting... done.  (wait exited with: $?)"

# This ls command ALWAYS fails in the real
# program in the 1st while-iteration, complaining about 
# missing files, but works in the 2nd iteration!
#
# It always works in the very 1st while-iteration of the
# abstracted version.
#
while ! ls -l /tmp/foo.*; do
    :
done

在我的原始程序中(而不是上面的抽象版本中),我确实在stdout上看到Waiting... done. (wait exited with: 0),就像在上面的版本中看到的那样。然而,ls -l总是会在原始版本中失败,但始终会在第一个while循环迭代中以上述抽象版本运行。

此外,尽管在标准输出上看到ls消息,但Instance 1 ended命令仍然失败。输出为:

$ ./myProgram
Launching instance 1
Waiting...
Waiting... done. (wait exited with: 0)
Instance 1 ended
ls: cannot access '/tmp/foo.*': No such file or directory
/tmp/foo.1
$

我注意到,如果在原始程序中将sleep 1放在ls的前面,就可以安全地消除while循环,就像这样:

# This too works in the original program:
sleep 1
ls -l /tmp/foo.*

问题:为什么wait不能在我的原始程序中按预期工作?有什么建议至少可以帮助您解决问题吗?

我在Ubuntu 18.04上使用bash 4.4.19

编辑:我还验证了失败的原始程序中对wait的调用正在退出,并且状态码为0

编辑2: Instance 1 ended之前是否不会出现Waiting... done. (wait exited with: 0)消息?在bash中处理后台进程时,这可能是OS的磁盘缓冲区/缓存的“刷新问题”吗?

编辑3:如果我不是发出while循环或sleep 1骇客,而是发出sync命令,那么,它起作用了!但是,为什么我必须在一个程序中而不是另一个程序中做一个sync

1 个答案:

答案 0 :(得分:0)

我注意到以下三个骇客都能正常工作,但并不确定原因:

Hack 1

while ! ls -l /tmp/foo.*; do
    :
done

Hack 2

sleep 1
ls -l /tmp/foo.*

Hack 3

sync
ls -l /tmp/foo.*

这是否是OS的磁盘缓冲区/缓存的“刷新问题”,尤其是在处理后台进程时,尤其是在bash中?换句话说,对wait的调用似乎在刷新磁盘缓存之前返回(或者,在操作系统本身意识到并完成刷新磁盘缓存之前)。

编辑感谢@Jon,他的猜测非常接近,让我朝着正确的方向进行思考,以及@chepner提供的古老的,细微的调整建议。

真正的问题:我开始foo时,不是直接/简单地(如原始问题中不准确的抽象版本所示),而是通过另一个launchThread函数,做完簿记后,在其正文中还会说foo 1 &。并且对launchThread的呼叫本身带有一个&后缀!因此,我的wait确实在等待launchThread而不是在foo上! sleepsyncwhile只是在帮助争取更多的时间来完成foo,这就是引入它们的原因。以下是对问题的更准确的说明,即使您可能会也可能无法在自己的系统上复制它(由于系统之间的调度/定时差异):

#!/bin/bash -u

function now() {
    date +'%Y-%m-%d %H:%M:%S'
}

function log() {
    echo "$(now) - $@" >> $logDir/log # Line 1
}

function foo() {
    local msg=$1
    log "$msg"
    echo "  foo ended"
}

function launchThread() {
    local f=$1
    shift
    "$f" "$@" &  # Line 2
}

logDir=/tmp/log

/bin/rm -rf "$logDir"
mkdir -p "$logDir"

echo "Launching foo..."
launchThread foo 'message abc' &  # Line 3

echo "Waiting for foo to finish..."
wait
echo "Waiting for foo to finish... done. (wait exited with: $?)"

ls "$logDir"/log*

上述越野车程序的输出:

Launching foo...
Waiting for foo to finish...
Waiting for foo to finish... done. (wait exited with: 0)
  foo ended
ls: cannot access '/tmp/log/log*': No such file or directory

如果我从&Line 2中删除Line 3,程序将正常工作,并输出以下内容:

Launching foo...
Waiting for foo to finish...
  foo ended
Waiting for foo to finish... done. (wait exited with: 0)
/tmp/log/log

如果我从$(now)中删除了Line 1部分,该程序也可以正常工作。