运行 进入竞争状态,即使 'wait'

Running into a race-condition, even with a 'wait'

我在 bash 程序中遇到了一个奇怪的竞争条件。我尝试通过一个足够简单的演示程序复制它,但显然,对于 all/most 计时相关的比赛演示尝试,我做不到。

这是该程序的抽象版本,不会重复该问题,但让我仍然解释一下:

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

    # [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

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

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

$ ./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
$

我注意到如果我在我原来的程序中 ls 之前放置一个 sleep 1 就可以安全地取消 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 中的后台进程时,这可能是 'flushing problem' 和 OS' disk-buffer/cache 吗?

编辑 3: 如果不是 while 循环或 sleep 1 黑客,我发出 sync 命令,那么,瞧,有用!但是,为什么我必须在一个程序中执行 sync 而在另一个程序中执行?

我注意到以下三个 hack 都有效,但不太清楚为什么:

技巧 1

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

技巧 2

sleep 1
ls -l /tmp/foo.*

技巧 3

sync
ls -l /tmp/foo.*

这会不会是 'flushing problem' 和 OS' disk-buffer/cache,特别是在处理后台进程时,尤其是在 bash 中?换句话说,对 wait 的调用似乎在它刷新磁盘缓存之前返回(或者,在 OS 自己意识到并完成刷新磁盘缓存之前)。

编辑感谢@Jon,他的猜测非常接近,让我思考正确的方向,以及来自@的古老的、按位调整的建议切普纳。

真正的问题: 我开始 foo,而不是 directly/plainly,如我在原始问题中不准确的抽象版本所示,而是通过另一个 launchThread 函数,在做一些簿记之后,它的主体也会说 foo 1 & 。对 launchThread 的调用本身带有 & 后缀!所以,我的 wait 真的在 launchThread 而不是 foo 等待! sleepsyncwhile 只是在帮助 foo 争取更多的时间来完成,这就是引入它们的原因。以下是对该问题的更准确的演示,即使您可能会或可能不会在您自己的系统上复制它(由于 scheduling/timing 系统间的差异):

#!/bin/bash -u

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

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

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

function launchThread() {
    local f=
    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 2Line 3 中删除 &,程序将正常运行,输出如下:

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

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