批处理文件多任务处理与 parent 共享变量的问题
Batch file multitasking issues sharing variables with parent
我正在尝试在 x265 中对视频文件进行批量编码,并想提出一个可以自动执行该过程的批处理文件。为了加快速度,我发现用 2 个线程调用 3 个 ffmpeg 实例会产生理想的编码时间,但是我昨天一整天都在尝试想出一种方法来获取一个批处理文件,该文件将调用 3 个实例,然后完成后调用新的。到目前为止,这是我所在的位置:
PARENT 批量
@echo off
Setlocal EnableDelayedExpansion
SET /A COUNT=0
for %%a in (*.mkv) do (
CALL :CHECK
SET /A COUNT+=1
START CALL "child.bat" "%%a"
)
EXIT /B 0
:CHECK
IF !COUNT! EQU 3 (
TIMEOUT /T 5
GOTO :CHECK
)
EXIT /B 0
CHILD 批量
ffmpeg <COMMAND LINE ARGS>
SET /A COUNT-=1
EXIT /B 0
我有两个问题。 1) COUNT 变量未在 parent 进程中更新,并且当 child 进程完成时它永远不会产生新实例。 2) child 进程没有完全退出。它留下一个单独的 cmd.exe window 打开并带有 DOS 提示符。
有什么想法吗?
编辑:替换了嵌套的 GOTO 以防止 FOR 循环中断
下面的解决方法
Setlocal EnableDelayedExpansion
SET /A COUNT=0
for %%a in (*.mkv) do (
IF !COUNT! EQU 3 CALL :CHECK
SET /A COUNT+=1
START CALL "child.bat" "%%a"
)
EXIT /B 0
:CHECK
IF EXIST DONE.TXT (
SET /A COUNT-=1
DEL DONE.TXT
EXIT /B 0
) ELSE (
TIMEOUT /T 5
GOTO :CHECK
)
EXIT /B 0
CHILD 批量
ffmpeg <COMMAND LINE ARGS>
:DONE
IF EXIST DONE.TXT (
TIMEOUT /T 1
GOTO :DONE
)
echo 1 >> DONE.TXT
EXIT 0
关于您所述的问题:
1) child进程不能修改parent进程中的环境变量。您将需要一种不同的机制来检测 child 何时终止。此外,正如 Squashman 在他的评论中所述,循环中的 GOTO 将中断(终止)循环,这就是为什么在前 3.
之后没有启动新的 child 进程的原因
2) 您的 child window 没有终止,因为您使用了 EXIT /B。请改用 EXIT,window 将关闭。
在找到可行的解决方案之前,您还有很长的路要走;-)
也许最大的障碍是检测 child 进程何时终止。
我知道 3 个策略:
1) 使用TASKLIST 结合FIND /C 来统计当前运行 的ffmpeg 进程数。这可能是最简单的解决方案,但它无法区分您的脚本启动的进程与可能由其他机制启动的进程。
2) 使用文件作为信号。为每个进程创建一个空文件,然后当进程完成时,让它删除该文件。您的主脚本可以通过查找文件来监视哪些进程处于活动状态。这也很简单,但如果您的某个进程在删除文件之前崩溃,它就不会正常运行。这会使您的系统处于不健康状态。
3) 我最喜欢使用锁定文件。每个 child 进程通过重定向锁定一个文件,当进程终止时(崩溃,正常退出,不管怎样),然后释放锁。主进程可以尝试锁定相同的文件。如果锁定成功,它知道 child 已经终止,否则 child 仍然是 运行。这个策略是最复杂的,它使用神秘的语法,但我觉得它非常有效。
我已经在 Parallel execution of shell processes 实施了一个使用选项 3) 的解决方案。以下是针对您的情况的 adaptation/simplification 代码。
我使用 START /B 在 parent window 中启动每个 child 进程,并将所有输出重定向到锁定文件。完成后,我键入输出文件,以便您查看发生了什么。我还列出了每个 child 进程的开始和停止时间。
您只需调整前 3 个环境变量以满足您的需要。剩下的应该很好去。但是,如果任何文件名包含 !
字符,则编写的代码将失败。可以通过更多的工作来删除此限制。
脚本中包含大量文档。 %= COMMENT =%
语法是一种在不使用 REM 的情况下安全地将注释嵌入循环的方法。
@echo off
setlocal enableDelayedExpansion
:: Define the command that will be run to obtain the list of files to process
set listCmd=dir /b /a-d *.mkv
:: Define the command to run for each file, where "%%F" is an iterated file name from the list
:: something like YOUR_COMMAND -i "%%F"
set runCmd=ffmpeg [** command arguments go here **]
:: Define the maximum number of parallel processes to run.
set "maxProc=3"
::---------------------------------------------------------------------------------
:: The remainder of the code should remain constant
::
:: Get a unique base lock name for this particular instantiation.
:: Incorporate a timestamp from WMIC if possible, but don't fail if
:: WMIC not available. Also incorporate a random number.
set "lock="
for /f "skip=1 delims=-+ " %%T in ('2^>nul wmic os get localdatetime') do (
set "lock=%%T"
goto :break
)
:break
set "lock=%temp%\lock%lock%_%random%_"
:: Initialize the counters
set /a "startCount=0, endCount=0"
:: Clear any existing end flags
for /l %%N in (1 1 %maxProc%) do set "endProc%%N="
:: Launch the commands in a loop
set launch=1
for /f "tokens=* delims=:" %%F in ('%listCmd%') do (
if !startCount! lss %maxProc% (
set /a "startCount+=1, nextProc=startCount"
) else (
call :wait
)
set cmd!nextProc!=%runCmd%
echo -------------------------------------------------------------------------------
echo !time! - proc!nextProc!: starting %runCmd%
2>nul del %lock%!nextProc!
%= Redirect the lock handle to the lock file. The CMD process will =%
%= maintain an exclusive lock on the lock file until the process ends. =%
start /b "" cmd /c >"%lock%!nextProc!" 2^>^&1 %runCmd%
)
set "launch="
:wait
:: Wait for procs to finish in a loop
:: If still launching then return as soon as a proc ends
:: else wait for all procs to finish
:: redirect stderr to null to suppress any error message if redirection
:: within the loop fails.
for /l %%N in (1 1 %startCount%) do 2>nul (
%= Redirect an unused file handle to the lock file. If the process is =%
%= still running then redirection will fail and the IF body will not run =%
if not defined endProc%%N if exist "%lock%%%N" 9>>"%lock%%%N" (
%= Made it inside the IF body so the process must have finished =%
echo ===============================================================================
echo !time! - proc%%N: finished !cmd%%N!
type "%lock%%%N"
if defined launch (
set nextProc=%%N
exit /b
)
set /a "endCount+=1, endProc%%N=1"
)
)
if %endCount% lss %startCount% (
timeout /t 1 /nobreak >nul
goto :wait
)
2>nul del %lock%*
echo ===============================================================================
echo Thats all folks
带有随机 window 标题的简单解决方案(基于 user2956477 的想法):
set windowtitle=Worker_%random%%random%%random%
start /min cmd /c "title %windowtitle% & dosomething.bat file1"
start /min cmd /c "title %windowtitle% & dosomething.bat file2"
start /min cmd /c "title %windowtitle% & dosomethingelse.exe"
timeout 3 >nul
:LOOP
tasklist /V /FI "imagename eq cmd.exe" /FI "windowtitle eq %windowtitle%*" | findstr %windowtitle% >nul
if "%errorlevel%"=="0" timeout 1 >nul && goto LOOP
我正在尝试在 x265 中对视频文件进行批量编码,并想提出一个可以自动执行该过程的批处理文件。为了加快速度,我发现用 2 个线程调用 3 个 ffmpeg 实例会产生理想的编码时间,但是我昨天一整天都在尝试想出一种方法来获取一个批处理文件,该文件将调用 3 个实例,然后完成后调用新的。到目前为止,这是我所在的位置:
PARENT 批量
@echo off
Setlocal EnableDelayedExpansion
SET /A COUNT=0
for %%a in (*.mkv) do (
CALL :CHECK
SET /A COUNT+=1
START CALL "child.bat" "%%a"
)
EXIT /B 0
:CHECK
IF !COUNT! EQU 3 (
TIMEOUT /T 5
GOTO :CHECK
)
EXIT /B 0
CHILD 批量
ffmpeg <COMMAND LINE ARGS>
SET /A COUNT-=1
EXIT /B 0
我有两个问题。 1) COUNT 变量未在 parent 进程中更新,并且当 child 进程完成时它永远不会产生新实例。 2) child 进程没有完全退出。它留下一个单独的 cmd.exe window 打开并带有 DOS 提示符。
有什么想法吗?
编辑:替换了嵌套的 GOTO 以防止 FOR 循环中断
下面的解决方法
Setlocal EnableDelayedExpansion
SET /A COUNT=0
for %%a in (*.mkv) do (
IF !COUNT! EQU 3 CALL :CHECK
SET /A COUNT+=1
START CALL "child.bat" "%%a"
)
EXIT /B 0
:CHECK
IF EXIST DONE.TXT (
SET /A COUNT-=1
DEL DONE.TXT
EXIT /B 0
) ELSE (
TIMEOUT /T 5
GOTO :CHECK
)
EXIT /B 0
CHILD 批量
ffmpeg <COMMAND LINE ARGS>
:DONE
IF EXIST DONE.TXT (
TIMEOUT /T 1
GOTO :DONE
)
echo 1 >> DONE.TXT
EXIT 0
关于您所述的问题:
1) child进程不能修改parent进程中的环境变量。您将需要一种不同的机制来检测 child 何时终止。此外,正如 Squashman 在他的评论中所述,循环中的 GOTO 将中断(终止)循环,这就是为什么在前 3.
之后没有启动新的 child 进程的原因2) 您的 child window 没有终止,因为您使用了 EXIT /B。请改用 EXIT,window 将关闭。
在找到可行的解决方案之前,您还有很长的路要走;-)
也许最大的障碍是检测 child 进程何时终止。
我知道 3 个策略:
1) 使用TASKLIST 结合FIND /C 来统计当前运行 的ffmpeg 进程数。这可能是最简单的解决方案,但它无法区分您的脚本启动的进程与可能由其他机制启动的进程。
2) 使用文件作为信号。为每个进程创建一个空文件,然后当进程完成时,让它删除该文件。您的主脚本可以通过查找文件来监视哪些进程处于活动状态。这也很简单,但如果您的某个进程在删除文件之前崩溃,它就不会正常运行。这会使您的系统处于不健康状态。
3) 我最喜欢使用锁定文件。每个 child 进程通过重定向锁定一个文件,当进程终止时(崩溃,正常退出,不管怎样),然后释放锁。主进程可以尝试锁定相同的文件。如果锁定成功,它知道 child 已经终止,否则 child 仍然是 运行。这个策略是最复杂的,它使用神秘的语法,但我觉得它非常有效。
我已经在 Parallel execution of shell processes 实施了一个使用选项 3) 的解决方案。以下是针对您的情况的 adaptation/simplification 代码。
我使用 START /B 在 parent window 中启动每个 child 进程,并将所有输出重定向到锁定文件。完成后,我键入输出文件,以便您查看发生了什么。我还列出了每个 child 进程的开始和停止时间。
您只需调整前 3 个环境变量以满足您的需要。剩下的应该很好去。但是,如果任何文件名包含 !
字符,则编写的代码将失败。可以通过更多的工作来删除此限制。
脚本中包含大量文档。 %= COMMENT =%
语法是一种在不使用 REM 的情况下安全地将注释嵌入循环的方法。
@echo off
setlocal enableDelayedExpansion
:: Define the command that will be run to obtain the list of files to process
set listCmd=dir /b /a-d *.mkv
:: Define the command to run for each file, where "%%F" is an iterated file name from the list
:: something like YOUR_COMMAND -i "%%F"
set runCmd=ffmpeg [** command arguments go here **]
:: Define the maximum number of parallel processes to run.
set "maxProc=3"
::---------------------------------------------------------------------------------
:: The remainder of the code should remain constant
::
:: Get a unique base lock name for this particular instantiation.
:: Incorporate a timestamp from WMIC if possible, but don't fail if
:: WMIC not available. Also incorporate a random number.
set "lock="
for /f "skip=1 delims=-+ " %%T in ('2^>nul wmic os get localdatetime') do (
set "lock=%%T"
goto :break
)
:break
set "lock=%temp%\lock%lock%_%random%_"
:: Initialize the counters
set /a "startCount=0, endCount=0"
:: Clear any existing end flags
for /l %%N in (1 1 %maxProc%) do set "endProc%%N="
:: Launch the commands in a loop
set launch=1
for /f "tokens=* delims=:" %%F in ('%listCmd%') do (
if !startCount! lss %maxProc% (
set /a "startCount+=1, nextProc=startCount"
) else (
call :wait
)
set cmd!nextProc!=%runCmd%
echo -------------------------------------------------------------------------------
echo !time! - proc!nextProc!: starting %runCmd%
2>nul del %lock%!nextProc!
%= Redirect the lock handle to the lock file. The CMD process will =%
%= maintain an exclusive lock on the lock file until the process ends. =%
start /b "" cmd /c >"%lock%!nextProc!" 2^>^&1 %runCmd%
)
set "launch="
:wait
:: Wait for procs to finish in a loop
:: If still launching then return as soon as a proc ends
:: else wait for all procs to finish
:: redirect stderr to null to suppress any error message if redirection
:: within the loop fails.
for /l %%N in (1 1 %startCount%) do 2>nul (
%= Redirect an unused file handle to the lock file. If the process is =%
%= still running then redirection will fail and the IF body will not run =%
if not defined endProc%%N if exist "%lock%%%N" 9>>"%lock%%%N" (
%= Made it inside the IF body so the process must have finished =%
echo ===============================================================================
echo !time! - proc%%N: finished !cmd%%N!
type "%lock%%%N"
if defined launch (
set nextProc=%%N
exit /b
)
set /a "endCount+=1, endProc%%N=1"
)
)
if %endCount% lss %startCount% (
timeout /t 1 /nobreak >nul
goto :wait
)
2>nul del %lock%*
echo ===============================================================================
echo Thats all folks
带有随机 window 标题的简单解决方案(基于 user2956477 的想法):
set windowtitle=Worker_%random%%random%%random%
start /min cmd /c "title %windowtitle% & dosomething.bat file1"
start /min cmd /c "title %windowtitle% & dosomething.bat file2"
start /min cmd /c "title %windowtitle% & dosomethingelse.exe"
timeout 3 >nul
:LOOP
tasklist /V /FI "imagename eq cmd.exe" /FI "windowtitle eq %windowtitle%*" | findstr %windowtitle% >nul
if "%errorlevel%"=="0" timeout 1 >nul && goto LOOP