【bat】バッチファイルで並列処理を実行する方法|同時実行数の制限・完了待ち・ログ出力

【bat】バッチファイルで並列処理を実行する方法|同時実行数の制限・完了待ち・ログ出力 bat

バッチファイルの処理は通常、上から1行ずつ順番に実行されます。しかし start コマンドを使えば、複数の処理を同時に(並列に)実行できます。

この記事では、start コマンドの基本と注意点全プロセスの完了待ち同時実行数の制限ログ出力の競合対策各プロセスの終了コード取得まで、並列処理に必要な知識を体系的に解説します。

スポンサーリンク

並列実行の基本:start コマンド

start コマンドは、プログラムを別プロセスとして起動し、バッチファイルは終了を待たずに次の行に進みます。

新しいウィンドウで並列実行

@echo off
start "" "C:MyApp	ask1.exe"
start "" "C:MyApp	ask2.exe"
start "" "C:MyApp	ask3.exe"
echo 3つのタスクを起動しました

3つのプロセスがそれぞれ別のウィンドウで同時に実行されます。echo はすべての起動直後に実行されます(終了を待ちません)。

バックグラウンドで並列実行(/b オプション)

/b を付けると、新しいウィンドウを開かずに同じコマンドプロンプト内でバックグラウンド実行します。

@echo off
start /b "" cmd /c "echo 処理A開始 & timeout /t 5 >nul & echo 処理A完了"
start /b "" cmd /c "echo 処理B開始 & timeout /t 3 >nul & echo 処理B完了"
echo 両方を起動しました
pause

/b の場合、各プロセスの出力がすべて同じウィンドウに混在して表示されます。

start と start /b の違い

項目 start(デフォルト) start /b
ウィンドウ 新規ウィンドウが開く 同じウィンドウ内
出力 各ウィンドウに独立表示 すべて混在して表示
Ctrl+C 各ウィンドウで個別に停止可能 親プロセスのみ停止(子は残る)
主な用途 GUI アプリの起動 CUI の軽量な並列処理

start コマンドの落とし穴

start で並列処理を書くとき、知らないとハマるポイントが3つあります。

落とし穴1:ダブルクォーテーションがタイトルになる

start は最初のダブルクォーテーションをウィンドウタイトルとして解釈します。

rem NG: パスがタイトルとして解釈される
start "C:Program Filesapp	ool.exe"

rem OK: 空のタイトル "" を先に置く
start "" "C:Program Filesapp	ool.exe"

start コマンドでは常に先頭に "" を書くのが鉄則です。

落とし穴2:内部コマンドや複合処理は cmd /c で包む

echocopydel などの内部コマンドや、& で繋いだ複合処理を start で並列実行するには、cmd /c で包む必要があります。

rem NG: echo は内部コマンドなので start 単体では動かない
start "" echo hello

rem OK: cmd /c で包む
start "" cmd /c "echo hello & pause"

落とし穴3:start /b のプロセスは Ctrl+C で止まらない

start /b で起動した子プロセスは、親ウィンドウで Ctrl+C を押しても停止しません。強制終了するには taskkill を使います。

rem プロセス名で強制終了
taskkill /f /im task1.exe

rem 子プロセスを含めてツリーごと終了
taskkill /f /t /im task1.exe

全プロセスの完了を待つ(tasklist 監視)

並列実行の最大の課題は「全部終わったか」を判定することです。最も確実な方法は tasklist でプロセスの存在をポーリング監視することです。

基本パターン

@echo off
echo タスクを並列実行します...
start "" "C:MyApp	ask1.exe"
start "" "C:MyApp	ask2.exe"
start "" "C:MyApp	ask3.exe"

:WAIT
timeout /t 2 /nobreak >nul
tasklist /fi "imagename eq task1.exe" 2>nul | find "task1.exe" >nul && goto WAIT
tasklist /fi "imagename eq task2.exe" 2>nul | find "task2.exe" >nul && goto WAIT
tasklist /fi "imagename eq task3.exe" 2>nul | find "task3.exe" >nul && goto WAIT

echo 全タスクが完了しました

仕組みtasklist でプロセスを検索し、find で見つかれば(ERRORLEVEL=0)&& でループに戻ります。全プロセスが見つからなくなったらループを抜けます。

タイムアウト付きの完了待ち

プロセスがハングした場合に備えて、上限時間を設けて強制終了するパターンです。

@echo off
setlocal enabledelayedexpansion
set MAX_WAIT=300
set INTERVAL=5
set ELAPSED=0

start "" "C:MyAppheavy_task.exe"

:POLL
timeout /t %INTERVAL% /nobreak >nul
set /a ELAPSED+=%INTERVAL%

tasklist /fi "imagename eq heavy_task.exe" 2>nul | find "heavy_task.exe" >nul
if !ERRORLEVEL! neq 0 (
    echo 完了しました(%ELAPSED%秒)
    goto DONE
)
if !ELAPSED! geq %MAX_WAIT% (
    echo タイムアウト(%MAX_WAIT%秒): 強制終了します
    taskkill /f /im heavy_task.exe >nul 2>&1
    goto DONE
)
goto POLL

:DONE
endlocal

注意:ループ内で ERRORLEVEL を正しく判定するには、setlocal enabledelayedexpansion を宣言して !ERRORLEVEL! を使います。%ERRORLEVEL% はループ突入時の値のまま更新されません。

同時実行数を制限する

大量のタスクを一度に start すると、CPU やメモリを使い切ってかえって遅くなります。同時実行数を制限して、空きが出たら次のタスクを投入する仕組みが必要です。

tasklist カウント方式

tasklist + find /c で実行中のプロセス数を数え、上限未満になるまで待機します。

@echo off
setlocal enabledelayedexpansion

rem 同時実行の上限
set MAXJOBS=4
set PROCESS_NAME=converter.exe

rem 処理対象のファイル一覧
for %%F in (C:data*.csv) do (
    :SLOT_CHECK
    for /f %%C in ('tasklist /fi "imagename eq %PROCESS_NAME%" ^| find /c "%PROCESS_NAME%"') do set COUNT=%%C
    if !COUNT! geq %MAXJOBS% (
        timeout /t 2 /nobreak >nul
        goto SLOT_CHECK
    )
    echo 起動: %%~nxF
    start "" "%PROCESS_NAME%" "%%F"
)

rem 全プロセスの終了を待つ
:WAIT_ALL
timeout /t 2 /nobreak >nul
tasklist /fi "imagename eq %PROCESS_NAME%" 2>nul | find "%PROCESS_NAME%" >nul && goto WAIT_ALL

echo 全ファイルの処理が完了しました
endlocal

MAXJOBS=4 を変更すれば並列度を調整できます。CPU のコア数に合わせるのが目安です。

ロックファイル(スロット)方式

もう1つの方法は、空のファイルをスロットとして使い、ファイルの有無で空きを判定する方式です。

@echo off
setlocal enabledelayedexpansion
set MAXJOBS=4
set SLOT_DIR=%TEMP%atch_slots

rem スロットディレクトリを初期化
if exist "%SLOT_DIR%" rd /s /q "%SLOT_DIR%"
md "%SLOT_DIR%"

for %%F in (C:data*.csv) do (
    :FIND_SLOT
    set FOUND_SLOT=
    for /l %%S in (1,1,%MAXJOBS%) do (
        if not exist "%SLOT_DIR%slot%%S.lock" (
            if not defined FOUND_SLOT (
                set FOUND_SLOT=%%S
            )
        )
    )
    if not defined FOUND_SLOT (
        timeout /t 1 /nobreak >nul
        goto FIND_SLOT
    )

    echo スロット!FOUND_SLOT!で起動: %%~nxF
    echo. > "%SLOT_DIR%slot!FOUND_SLOT!.lock"
    start "" cmd /c "converter.exe "%%F" & del "%SLOT_DIR%slot!FOUND_SLOT!.lock""
)

rem 全スロットの解放を待つ
:WAIT_SLOTS
timeout /t 1 /nobreak >nul
dir /b "%SLOT_DIR%*.lock" >nul 2>&1 && goto WAIT_SLOTS

rd /s /q "%SLOT_DIR%"
echo 全処理が完了しました
endlocal

各プロセスが終了時に自分のロックファイルを削除するため、プロセス名が同じでも正確に空きを判定できます。

並列処理時のログ出力の競合対策

複数のプロセスが同じログファイルに同時に書き込むと、内容が混在したり、書き込みに失敗したりします。

対策1:プロセスごとに個別ログを出力する(推奨)

@echo off
for %%F in (C:data*.csv) do (
    start "" cmd /c "converter.exe "%%F" > "C:logs\%%~nF.log" 2>&1"
)

各プロセスが独立したファイルに出力するため、競合は発生しません。

対策2:処理後にログを統合する

個別ログを最後に1つのファイルにまとめます。

rem 全プロセス完了後に統合
type C:logs*.log > C:logsall_results.log

対策3:start なし(新ウィンドウ方式)で出力分離

start /b ではなく start(新ウィンドウ)を使えば、各ウィンドウの出力は独立します。ただしウィンドウが大量に開くため、ファイルへのリダイレクトと組み合わせるのが実用的です。

各プロセスの終了コードを取得する

start で起動したプロセスの ERRORLEVEL は、そのままでは取得できません。各プロセス内で終了コードをファイルに書き出す方法が最も確実です。

@echo off
setlocal
set RESULT_DIR=%TEMP%atch_results
if exist "%RESULT_DIR%" rd /s /q "%RESULT_DIR%"
md "%RESULT_DIR%"

rem 各タスクを起動(終了コードをファイルに記録する)
start "" cmd /c ""C:MyApp	ask1.exe" & echo %^^ERRORLEVEL% > "%RESULT_DIR%	ask1.txt""
start "" cmd /c ""C:MyApp	ask2.exe" & echo %^^ERRORLEVEL% > "%RESULT_DIR%	ask2.txt""
start "" cmd /c ""C:MyApp	ask3.exe" & echo %^^ERRORLEVEL% > "%RESULT_DIR%	ask3.txt""

rem 全プロセスの完了を待つ
:WAIT
timeout /t 2 /nobreak >nul
if not exist "%RESULT_DIR%	ask1.txt" goto WAIT
if not exist "%RESULT_DIR%	ask2.txt" goto WAIT
if not exist "%RESULT_DIR%	ask3.txt" goto WAIT

rem 結果を集計する
echo === 実行結果 ===
set ALL_OK=1
for %%T in (task1 task2 task3) do (
    set /p RESULT=<"%RESULT_DIR%\%%T.txt"
    for /f %%R in ("%RESULT_DIR%\%%T.txt") do set RESULT=%%R
    if not "!RESULT!"=="0" (
        echo %%T : 失敗(コード: !RESULT!)
        set ALL_OK=0
    ) else (
        echo %%T : 成功
    )
)

if "%ALL_OK%"=="1" (
    echo 全タスク正常終了
) else (
    echo 一部タスクが失敗しました
)

rd /s /q "%RESULT_DIR%"
endlocal

ポイント%^^ERRORLEVEL%^^ はエスケープです。cmd /c に渡す文字列内では % が即時展開されるため、^^ で遅延させて子プロセス内で評価させます。

for ループで大量タスクを並列化する

フォルダ内のファイルを一括処理するなど、タスク数が不定の場合は for ループと組み合わせます。

@echo off
setlocal enabledelayedexpansion
set MAXJOBS=4
set PROCESS=ffmpeg.exe
set LOG_DIR=C:logs

if not exist "%LOG_DIR%" md "%LOG_DIR%"

for %%F in (C:videos*.avi) do (
    :CHECK_SLOT
    for /f %%C in ('tasklist /fi "imagename eq %PROCESS%" ^| find /c "%PROCESS%"') do set RUNNING=%%C
    if !RUNNING! geq %MAXJOBS% (
        timeout /t 2 /nobreak >nul
        goto CHECK_SLOT
    )
    echo 変換開始: %%~nxF
    start "" "%PROCESS%" -i "%%F" "C:output\%%~nF.mp4" > "%LOG_DIR%\%%~nF.log" 2>&1
)

:WAIT_FINISH
timeout /t 2 /nobreak >nul
tasklist /fi "imagename eq %PROCESS%" 2>nul | find "%PROCESS%" >nul && goto WAIT_FINISH

echo 全動画の変換が完了しました
endlocal

この例では、AVI ファイルを最大4並列で MP4 に変換しています。MAXJOBS を変更するだけで並列度を調整できます。

実践テンプレート

テンプレート1:複数サーバーへの同時デプロイ

@echo off
setlocal enabledelayedexpansion
cd /d "%~dp0"

set DEPLOY_SCRIPT=deploy_to_server.bat
set LOG_DIR=%~dp0logs
if not exist "%LOG_DIR%" md "%LOG_DIR%"

echo === デプロイ開始 ===
for %%S in (server01 server02 server03 server04) do (
    echo   %%S にデプロイ中...
    start "" cmd /c "call %DEPLOY_SCRIPT% %%S > "%LOG_DIR%\%%S.log" 2>&1"
)

rem 全デプロイの完了を待つ
:WAIT_DEPLOY
timeout /t 5 /nobreak >nul
tasklist /fi "windowtitle eq %DEPLOY_SCRIPT%*" 2>nul | find "cmd.exe" >nul && goto WAIT_DEPLOY

echo.
echo === デプロイ結果 ===
for %%S in (server01 server02 server03 server04) do (
    findstr /i "error fail" "%LOG_DIR%\%%S.log" >nul 2>&1
    if !ERRORLEVEL! equ 0 (
        echo   %%S : 要確認(エラーあり)
    ) else (
        echo   %%S : 成功
    )
)
endlocal

テンプレート2:ログ付き並列処理(汎用)

タスク一覧をテキストファイルから読み込み、並列度を制限しながら実行する汎用テンプレートです。

@echo off
setlocal enabledelayedexpansion
cd /d "%~dp0"

rem === 設定 ===
set MAXJOBS=4
set TASK_LIST=tasks.txt
set LOG_DIR=%~dp0logs
set SUMMARY_LOG=%~dp0summary.log

if not exist "%LOG_DIR%" md "%LOG_DIR%"
echo [%date% %time%] 並列処理開始(最大%MAXJOBS%並列) > "%SUMMARY_LOG%"

set TASK_NO=0
for /f "usebackq tokens=*" %%L in ("%TASK_LIST%") do (
    set /a TASK_NO+=1

    :WAIT_SLOT
    set RUNNING=0
    for /f %%C in ('tasklist /fi "windowtitle eq PARALLEL_TASK_*" ^| find /c "cmd.exe"') do set RUNNING=%%C
    if !RUNNING! geq %MAXJOBS% (
        timeout /t 1 /nobreak >nul
        goto WAIT_SLOT
    )

    echo [!TASK_NO!] 起動: %%L
    echo [%date% %time%] 起動: %%L >> "%SUMMARY_LOG%"
    start "PARALLEL_TASK_!TASK_NO!" cmd /c "%%L > "%LOG_DIR%	ask_!TASK_NO!.log" 2>&1"
)

rem 全タスクの完了を待つ
:WAIT_ALL
timeout /t 2 /nobreak >nul
tasklist /fi "windowtitle eq PARALLEL_TASK_*" 2>nul | find "cmd.exe" >nul && goto WAIT_ALL

echo [%date% %time%] 全タスク完了 >> "%SUMMARY_LOG%"
echo.
echo === 全タスク完了 ===
echo 個別ログ: %LOG_DIR%
echo サマリー : %SUMMARY_LOG%
endlocal

tasks.txt の例

converter.exe "C:datafile1.csv" --output "C:outfile1.json"
converter.exe "C:datafile2.csv" --output "C:outfile2.json"
converter.exe "C:datafile3.csv" --output "C:outfile3.json"
robocopy "C:src" "D:ackup" /mir /mt:4

ウィンドウタイトルに PARALLEL_TASK_ というプレフィックスを付けることで、tasklist のフィルタで自分が起動したプロセスだけを正確に監視できます。

まとめ

やりたいこと 方法
複数プログラムを同時に起動 start "" を複数行書く
ウィンドウを開かずにバックグラウンド実行 start /b "" を使う
全プロセスの完了を待つ tasklist + ループ監視
同時実行数を制限する tasklist + find /c でカウント
ログ出力の競合を防ぐ プロセスごとに個別ログファイルへ出力
各プロセスの終了コードを取得 ファイルに書き出して後から集約
ハングしたプロセスを強制終了 taskkill /f /im プロセス名

ポイント

  • start の先頭には必ず空タイトル "" を書く(パスがタイトルとして誤認識される罠を防ぐ)
  • 内部コマンドや & で繋いだ処理は cmd /c で包む
  • 並列プロセスの完了待ちは tasklist + ループが最も確実
  • 大量タスクは同時実行数を制限して投入する(目安: CPUコア数)
  • ログはプロセスごとに個別ファイルに出力し、最後に統合する
  • ループ内の ERRORLEVELenabledelayedexpansion + !ERRORLEVEL! で取得する

あわせて読みたい