【问题标题】:Batch For loop doesn't refresh the file it's pulling fromBatch For 循环不会刷新它从中提取的文件
【发布时间】:2012-01-26 16:36:40
【问题描述】:

所以我有一个 for 循环,它为文件 queue.txt 中的每一行执行 SQL 存储过程的迭代,现在一切正常,但是如果它正在迭代并且另一行添加到它使用文件的底部作为它的迭代标准,然后它只是忽略它。

我拥有的是这样的:

@echo off
cd "%UserProfile%\Desktop\Scripting\"
echo words > busy.txt

FOR /f "delims=" %%a in ('type queue.txt') DO (
IF NOT EXIST reset.sql (

::Create SQL command
echo USE dbname> reset.sql
echo EXEC dbo.sp_ResetSubscription @ClientName = '%%a'>> reset.sql
echo EXEC dbo.sp_RunClientSnapshot @ClientName = '%%a'>> reset.sql
echo #################### %date% - %time% ####################################################>> log.txt
echo Reinitialising '%%a'>> log.txt
sqlcmd -i "reset.sql">> log.txt
echo. >> log.txt
echo ####################################################################################################>> log.txt
echo. >> log.txt

type queue.txt | findstr /v %%a> new.txt
type new.txt> queue.txt
echo New list of laptops waiting:>> log.txt
type queue.txt>> log.txt
echo. >> log.txt
echo ####################################################################################################>> log.txt
echo. >> log.txt

if exist reset.sql del /f /q reset.sql

) 
)

if exist busy.txt del /f /q busy.txt
if exist queue.txt del /f /q queue.txt
if exist new.txt del /f /q new.txt

所以它的作用是拉取文件queue.txt 并对其中的每一个进行迭代,现在说它从文件中的 2 行开始,这很好,它开始为它们运行过程。

现在,假设我在 queue.txt 中添加了另一行,而循环正在运行它只是忽略该行,因此看起来 for 不会在每次迭代时从文件中更新它只是导入一次。

我想解决这个问题的一种方法是在循环的第一次迭代中计算行数,然后在每次迭代结束时根据它认为的值检查它,如果它超过它期望然后返回到 for 循环之上(使用 goto 或类似的方法),但 goto 在逻辑表达式中不起作用。

请给任何人建议?

【问题讨论】:

    标签: loops batch-file for-loop


    【解决方案1】:

    @Myles Gray - 你的解决方案有一些问题。

    首先是小问题:

    1) 在队列循环的每次迭代之后,您重新创建队列作为原始队列减去您当前正在处理的行(您希望!稍后会详细介绍)。重新创建队列后,将其附加到日志中。这会起作用,但它似乎非常低效,并且有可能使日志变得庞大而笨重。假设您有一个包含 10,000 行的队列。到您处理队列时,您将在日志中写入 99,989,998 个队列行,其中包括 49,994,999 个队列行!即使没有实际工作,这也需要很长时间来处理。

    2) 您使用 FINDSTR 重新创建队列,保留所有与您当前 ID 不匹配的行。但是,如果它们碰巧与您当前的 ID 匹配,这也会删除后续行。那可能不是问题。但是你正在做一个子字符串匹配。您的 FINDSTR 还将消除在其中任何位置包含您当前 ID 的后续行。我不知道你的身份证是什么样子的。但是,如果您当前的 ID 是 123,那么以下所有 ID 都将被错误地剥离 - 31236、12365 等。这是一个潜在的毁灭性问题。我说这是可能的,因为 FOR 循环已经缓冲了队列,所以它不在乎 - 除非你中止循环,因为新的工作已附加到 late.txt 文件 - 那么你实际上会跳过那些丢失的 ID!这可以通过将 /X 选项添加到 FINDSTR 来解决。至少那样你只会跳过真正的重复。

    现在的主要问题 - 所有这些都源于只有一个进程可以为任何类型的写入(或删除)操作打开文件。

    3) 即使 FOR /F 循环不会写入文件,但如果文件正被另一个进程主动写入,它也会失败。因此,如果您的 FOR 循环尝试在另一个进程附加到队列时读取队列,您的队列处理脚本将失败。您已经检查了busy.txt 文件,但您的队列编写器可能在busy.txt 文件创建之前就已经开始写入。写入操作可能需要一段时间,尤其是在附加许多行的情况下。在写入行时,您的队列处理器可能会启动,然后您会遇到冲突和失败。

    4) 您的队列处理器将late.txt 附加到您的队列中,然后删除late.txt。但是在追加和删除之间有一个时间点,队列编写器可以在late.txt 中追加一行。这条迟到的行将被删除而不被处理!

    5) 另一种可能性是写入者可能会在队列处理器正在删除它的过程中尝试写入late.txt。写入将失败,您的队列将再次丢失工作。

    6) 另一种可能性是您的队列可能会在队列写入器附加到它时尝试删除late.txt。删除将失败,并且下次队列处理器将 late.txt 附加到 queue.txt 时,您的队列中将出现重复项。

    总之,并发问题可能会导致队列中的工作丢失,以及队列中的重复工作。每当您有多个进程同时对文件进行更改时,您必须建立某种锁定机制来序列化事件。

    您已经在使用 SqlServer 数据库。最合乎逻辑的做法是将队列移出文件系统并移入数据库。关系数据库是从头开始构建的以处理并发性。

    话虽如此,只要您采用锁定策略,在 Windows 批处理中将文件用作队列并不难。您必须确保队列处理器和队列编写器都遵循相同的锁定策略。

    以下是基于文件的解决方案。我将假设您只有一个队列处理器,并且可能有多个队列编写器。通过额外的工作,您可以调整文件队列解决方案以支持多个队列处理器。但是使用我在my first answer 末尾描述的基于文件夹的队列可能更容易实现多个队列处理器。

    与让队列编写器写入 queue.txt 或 late.txt 相比,让队列处理器重命名现有队列并将其处理到完成会更容易,而队列编写器始终写入 queue.txt。

    此解决方案将当前状态写入 status.txt 文件。您可以通过从命令窗口发出TYPE STATUS.TXT 来监控您的队列处理器状态。

    我会进行一些延迟扩展切换,以防止由于您的数据中的! 而导致损坏。如果您知道! 永远不会出现,那么您只需将 SETLOCAL EnableDelayedExpansion 移到顶部并放弃切换即可。

    另一个优化 - 为一组语句只重定向一次输出而不是为每个语句打开和关闭文件会更快。

    此代码完全未经测试,因此很容易出现一些愚蠢的错误。但概念是合理的。希望你能明白。

    queueProcessor.bat

    @echo off
    setlocal disableDelayedExpansion
    cd "%UserProfile%\Desktop\Scripting\"
    
    :rerun
    
    ::Safely get a copy of the current queue, exit if none or error
    call :getQueue || exit /b
    
    ::Get the number of lines in the queue to be used in status updates
    for /f %%n in ('find /v "" ^<inProcess.txt') do set /a "record=0, recordCount=%%n"
    
    ::Main processing loop
    for /f "delims=" %%a in (inProcess.txt) do (
    
      rem :: Update the status. Need delayed expansion to access the current record number.
      rem :: Need to toggle delayed expansion in case your data contains !
      setlocal enableDelayedExpansion
      set /a "record+=1"
      > status.txt echo processing !record! out of %recordCount%
      endlocal
    
      rem :: Create SQL command
      > reset.sql (
        echo USE dbname
        echo EXEC dbo.sp_ResetSubscription @ClientName = '%%a'
        echo EXEC dbo.sp_RunClientSnapshot @ClientName = '%%a'
      )
    
      rem :: Log this action and execute the SQL command
      >> log.txt (
        echo #################### %date% - %time% ####################################################
        echo Reinitialising '%%a'
        sqlcmd -i "reset.sql"
        echo.
        echo ####################################################################################################
        echo.
      )
    )
    
    ::Clean up
    delete inProcess.txt
    delete status.txt
    
    ::Look for more work
    goto :rerun
    
    :getQueue
    2>nul (
      >queue.lock (
        if not exist queue.txt exit /b 1
        if exist inProcess.txt (
          echo ERROR: Only one queue processor allowed at a time
          exit /b 2
        )
        rename queue.txt inProcess.txt
      )
    )||goto :getQueue
    exit /b 0
    

    queueWriter.bat

    ::Whatever your code is
    ::At some point you want to append a VALUE to the queue in a safe way
    call :appendQueue VALUE
    ::continue on until done
    exit /b
    
    :appendQueue
    2>nul (
      >queue.lock (
        >>queue.txt echo %*
      )
    )||goto :appendQueue
    

    锁码说明:

    :retry
    ::First redirect any error messages that occur within the outer block to nul
    2>nul (
    
      rem ::Next redirect all stdout within the inner block to queue.lock
      rem ::No output will actually go there. But the file will be created
      rem ::and this process will have a lock on the file until the inner
      rem ::block completes. Any other process that tries to write to this
      rem ::file will fail. If a different process already has queue.lock 
      rem ::locked, then this process will fail to get the lock and the inner
      rem ::block will not execute. Any error message will go to nul.
      >queue.lock (
    
        rem ::you can now safely manipulate your queue because you have an
        rem ::exclusive lock.
        >>queue.txt echo data 
    
        rem ::If some command within the inner block can fail, then you must
        rem ::clear the error at the end of the inner block. Otherwise this
        rem ::routine can get stuck in an endless loop. You might want to 
        rem ::add this to my code - it clears any error.
        verify >nul
    
      ) && (
    
        rem ::I've never done this before, but if the inner block succeeded,
        rem ::then I think you can attempt to delete queue.lock at this point.
        rem ::If the del succeeds then you know that no process has a lock
        rem ::at this point. This could be useful if you are trying to monitor
        rem ::the processes. If the del fails then that means some other process
        rem ::has already grabbed the lock. You need to clear the error at
        rem ::this point to prevent the endless loop
        del queue.lock || verify >nul
    
      )
    
    ) || goto :retry
    :: If the inner block failed to get the lock, then the conditional GOTO
    :: activates and it loops back to try again. It continues to loop until
    :: the lock succeeds. Note - the :retry label must be above the outer-
    :: most block.
    

    如果您有唯一的进程 ID,您可以将其写入内部块中的 queue.lock。然后,您可以从另一个窗口键入 queue.lock 以找出当前拥有(或最近拥有)锁的进程。只有在某些进程挂起时,这才是一个问题。

    【讨论】:

    • 哇,我只能说,出色的工作,你应该为此获得赏金,这是一个令人难以置信的工作量,我会解决这个问题,看看事情如何使用你的解决方案,但我的上帝,再次出色的工作!明天为你 +100!
    • @MylesGray - 酷。很高兴得到赞赏,而且赏金是一个意想不到的甜蜜奖金 - 假设我的回答得到最多的选票:) 代码并不难,因为我在 1980 年代做过一些基于文件系统的队列工作。但解释确实需要时间。 注意 - 自从我第一次发布以来,我在 queueWriter(条件 goto)的最后添加了一个快速错误修复。
    • 我可以提前结束它并奖励给你——这就是我正在等待的(需要 24 小时才能完成)。对于基于文件夹的解决方案,我假设我只是将 inProcess.txt、queue.txt 等与文件夹交换?你能解释一下2&gt;nul ( &gt;queue.lock 的工作原理吗?
    • @MylesGray - 现在我想,你可能会很好地留在基于文件的队列中。如果您决定支持多个队列处理器,那么基于文件夹的解决方案开始变得更有意义。
    • 再次感谢 - 我已授予您赏金!
    【解决方案2】:

    您是绝对正确的 - FOR /F 循环等待 IN() 子句中的命令完成并在处理第一行之前缓冲结果。如果您从 IN() 子句中的文件读取而不是执行命令,情况也是如此。

    如果您停止在 FOR 循环中处理队列内容,您提出的在 FOR 循环之前计算队列中的行数,然后在 FOR 循环完成后重新计数的策略几乎可以工作。如果最终计数大于原始计数,您可以在 FOR 循环之前转到 :label 并跳过 FOR 循环中的原始行数,因此您只处理附加的行。但是,如果一个进程在您获取行数时写入队列,或者如果它在您获得最终计数但在您删除队列之前附加到队列中,您仍然会遇到并发问题。

    在处理多个进程时,有一些方法可以在批处理中序列化事件。这样做的关键是利用一个事实,即只有一个进程可以打开文件以进行写访问。

    如下代码可用于建立独占“锁”。只要每个 进程使用相同的逻辑,您就可以保证您对一个或多个文件系统对象具有独占控制权,直到您通过退出代码块来释放锁定。

    :getLock
    2>nul (
      >lockName.lock (
        rem ::You now have an exclusive lock while you remain in this block of code
        rem ::You can safely count the number of lines in a queue file,
        rem ::or append lines to the queue file at this time.
      )
    )||goto :getLock
    

    我在Re: parallel process with batch 演示了它是如何工作的。按下链接后,向上滚动以查看原始问题。这似乎与您的问题非常相似。

    您可能需要考虑将文件夹用作队列而不是文件。每个工作单元都可以是文件夹中自己的文件。您可以使用锁来安全地递增文件中的序列号,以用于命名每个工作单元。您可以通过在“preperation”文件夹中准备工作单元来保证工作单元已完全写入,并且仅在完成后将其移动到“queue”文件夹。这种策略的优点是每个工作单元文件可以在处理过程中移动到“inProcess”文件夹,然后可以在完成时将其删除或移动到存档文件夹。如果处理失败,您可以恢复,因为该文件仍然存在于“inProcess”文件夹中。您可以知道哪些工作单元不稳定(“inProcess”文件夹中的死单元),以及哪些工作单元尚未处理(那些仍在“队列”文件夹中)。

    【讨论】:

    • 非常感谢,但我发现了一个更直接的方法(以一种迂回的方式),如果我的脚本中有什么东西死了,我会在文件夹中使用你的想法,很多更好!
    【解决方案3】:

    你把你的问题“如果另一行添加到文件的底部......”;但是,您的代码不会添加一行,而是完全替换整个文件内容(尽管新内容只添加了一个新行):

    FOR /f "delims=" %%a in ('type queue.txt') DO (
       IF NOT EXIST reset.sql (
    
       . . .
    
       type queue.txt | findstr /v %%a> new.txt
       rem Next line REPLACES the entire queue.txt file!
       type new.txt> queue.txt
       echo New list of laptops waiting:>> log.txt
    
       . . .
    
       if exist reset.sql del /f /q reset.sql
    
       ) 
    )
    

    您可以更改处理 queue.txt 文件的方法,方法是将其重定向到通过 SET /P 命令读取其行的子例程和使用 GOTO 组装的循环。这样,在读取循环中添加到 queue.txt 文件底部的行将在读取过程到达它们时立即被读取。

    call :ProcessQueue < queue.txt >> queue.txt
    goto :EOF
    
    
    :ProcessQueue
       set line=
       rem Next command read a line from queue.txt file:
       set /P line=
       if not defined line goto endProcessQueue
       rem In following code use %line% instead of %%a
       IF NOT EXIST reset.sql (
    
       . . .
    
       type queue.txt | findstr /v %%a> new.txt
       rem Next command ADD new lines to queue.txt file:
       type new.txt
       echo New list of laptops waiting:>> log.txt
    
       . . .
    
       if exist reset.sql del /f /q reset.sql
    
       ) 
    goto ProcessQueue
    :endProcessQueue
    exit /B
    

    当然,如果新行是由其他进程添加的,新行将被这个批处理文件自动读取和处理。

    你必须知道这个方法在 queue.txt 文件的第一个空行结束;它对可以处理的字符也有一些限制。

    编辑:这是一个简单的示例,展示了此方法的工作原理:

    set i=0
    call :ProcessQueue < queue.txt >> queue.txt
    goto :EOF
    
    :ProcessQueue
       set line=
       set /P line=
       if not defined line goto endProcessQueue
       echo Line processed: %line% > CON
       set /A i=i+1
       if %i% == 1 echo First line added to queue.txt
       if %i% == 2 echo Second line added to queue.txt
    goto ProcessQueue
    :endProcessQueue
    exit /B
    

    这是输入时的 queue.txt 文件:

    Original first line
    Original second line
    Original third line
    Original fourth line
    

    这是结果:

    Line processed: Original first line
    Line processed: Original second line
    Line processed: Original third line
    Line processed: Original fourth line
    Line processed: First line added to queue.txt
    Line processed: Second line added to queue.txt
    

    【讨论】:

    • 我知道它会替换整个文件,该部分所做的是检查 late.txt 是否有任何新行将它们添加到 new.txt 并将当前的 queue.txt 文件添加到 new.txt 然后覆盖queue.txt 的内容为new.txt.
    • @MylesGray:我刚刚注意到我的方法需要追加新行到queue.txt 文件,而不是替换它。这种方法很有趣,因为它完全避免了读取queue.txt 文件的进程与向其追加 新行的任何其他进程之间的所有并发问题,并且简单且快速。我添加了一个小示例,以便您了解我的方法可以做什么;该示例基于您的原始问题。
    【解决方案4】:

    好的,所以我解决的问题是添加一个名为co-ordinator.bat 的额外批处理文件,它检查busy.txt 是否存在,如果存在,那么它将连接设备添加到文件late.txt在循环的每次迭代结束时,该进程将检查是否存在late.txt,如果存在则将其与queue.txt 合并,然后使用循环外的goto 到顶部重新- 初始化 for 循环。

    代码如下:

    @echo off
    cd "%UserProfile%\Desktop\Scripting\"
    echo words > busy.txt
    :rerun
    
    FOR /f "delims=" %%a in ('type queue.txt') DO (
    IF NOT EXIST reset.sql (
    
    ::Create SQL command
    echo USE dbname> reset.sql
    echo EXEC dbo.sp_ResetSubscription @ClientName = '%%a'>> reset.sql
    echo EXEC dbo.sp_RunClientSnapshot @ClientName = '%%a'>> reset.sql
    echo #################### %date% - %time% ####################################################>> log.txt
    echo Reinitialising '%%a'>> log.txt
    sqlcmd -i "reset.sql">> log.txt
    echo. >> log.txt
    echo ####################################################################################################>> log.txt
    echo. >> log.txt
    
    type queue.txt | findstr /v %%a> new.txt
    type new.txt> queue.txt
    echo New list of laptops waiting:>> log.txt
    type queue.txt>> log.txt
    echo. >> log.txt
    echo ####################################################################################################>> log.txt
    echo. >> log.txt
    
    if exist reset.sql del /f /q reset.sql
    if exist late.txt (
    type late.txt>> queue.txt
    del /f /q late.txt
    goto rerun
    )
    ) 
    )
    
    if exist late.txt del /f /q late.txt
    if exist busy.txt del /f /q busy.txt
    if exist queue.txt del /f /q queue.txt
    if exist new.txt del /f /q new.txt
    

    【讨论】:

    • 您在使用此解决方案时遇到了一些重大问题。请参阅我的2nd answer 了解更多信息
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多