【bat】大量ファイルを並列処理する方法完全ガイド|start /b・スロット制御・完了待ち・ログ管理・優先度設定まで

バッチファイル(.bat)は基本的に1行ずつ順番に実行される「逐次処理」ですが、start コマンドを使うと 複数のコマンドを同時に起動する「並列処理」 が実現できます。100ファイルを1つずつ変換すると10分かかる処理が、4並列なら約2.5分に短縮できます。本記事では start /b の基本から、ロックファイルによるスロット制御・完了待ち・ジョブ別ログ・優先度設定まで外部ツール不要で実装する方法を体系的に解説します。

この記事で解決できること

  • start /b で複数コマンドを並列起動する基本パターン
  • 同時実行数を制限するスロット制御(暴走防止)
  • 全ジョブが完了するまで待機する方法
  • 各ジョブの標準出力・エラー・終了コードをファイルに記録する方法
  • 優先度(/low)・CPUコア割り当て(/affinity)で負荷を制御する方法
  • 大量ファイルを並列変換・並列チェックする実践パターン
スポンサーリンク

逐次処理と並列処理の比較

方式 コマンド 特徴 向き不向き
逐次処理 通常の for ループ 1件ずつ順番に実行 依存関係がある処理
並列起動(制御なし) start /b 全件を一気に起動 少数ファイル・軽い処理
並列処理(スロット制御) start /b + ロックファイル 同時実行数を上限で制御 大量ファイル・重い処理

start コマンドの主要オプション

オプション 意味 用途
/b 新しいウィンドウを開かずバックグラウンドで起動 並列処理の基本
/wait 起動したプロセスの終了を待つ 逐次実行(並列ではない)
/low 低優先度で起動 PC の通常操作を妨げない
/belownormal 通常より低い優先度 バックグラウンド処理向け
/normal 通常優先度(デフォルト) 標準
/high 高優先度 急ぎの処理
/affinity N 使用CPUコアを16進数マスクで指定 コア割り当て制御

方法1: start /b で基本的な並列起動

シンプルな並列起動

@echo off
rem 3つのジョブを並列起動(それぞれ独立して実行)
start "" /b cmd /c "echo [JOB1] 開始 & timeout /t 3 >nul & echo [JOB1] 完了"
start "" /b cmd /c "echo [JOB2] 開始 & timeout /t 2 >nul & echo [JOB2] 完了"
start "" /b cmd /c "echo [JOB3] 開始 & timeout /t 4 >nul & echo [JOB3] 完了"

echo 3つのジョブを起動しました(バックグラウンドで並列実行中)
echo このメッセージは即座に表示されます
start “” /b cmd /c の意味
start:新しいプロセスを起動
"":ウィンドウタイトル(省略するとコマンド名が使われてしまう)
/b:バックグラウンドで起動(新しいウィンドウを開かない)
cmd /c "...":コマンドを実行して終了(& で複数コマンドを連結)

for ループで大量ファイルを並列起動

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

set "SRC=.in"
set "OUT=.out"

if not exist "%OUT%" mkdir "%OUT%"

set COUNT=0

rem 全ファイルを一気に並列起動(ファイル数が少ない場合向け)
for %%F in ("%SRC%*.txt") do (
    echo 起動: %%~nxF
    start "" /b cmd /c "type ""%%~fF"" > ""%OUT%\%%~nF.out"" & echo [DONE] %%~nxF"
    set /a COUNT+=1
)

echo.
echo !COUNT! ジョブを並列起動しました
endlocal
制限なし並列起動の注意点
上記は全ファイルを一気に起動します。ファイルが数十〜数百件あると、同時プロセス数が爆発して I/O 競合やメモリ不足で逆に遅くなります。
大量ファイルには必ず スロット制御(同時実行数の上限)を設けてください。

方法2: スロット制御で同時実行数を制限する

ロックファイルを使ったスロット制御が、外部ツール不要で実装できる定番パターンです。仕組みは以下のとおりです。

ステップ 処理
スロットフォルダに ジョブ名.lock ファイルを作成(スロット取得)
start /b でジョブをバックグラウンド起動
ジョブ終了時に del でロックファイルを削除(スロット解放)
ロックファイル数が MAXJOBS 以上なら1秒待機してリトライ

スロット制御の実装

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

rem ==== 設定 ====
set "SRC=.in"
set "OUT=.out"
set MAXJOBS=4
set "SLOTS=%TEMPat-slots"

if not exist "%OUT%" mkdir "%OUT%"
if not exist "%SLOTS%" mkdir "%SLOTS%"

set TOTAL=0

rem ==== ジョブ投入ループ ====
for %%F in ("%SRC%*.txt") do (

    rem スロットが空くまで待機
    call :acquire_slot

    rem ロックファイルを作成(スロット取得)
    set "LOCK=%SLOTS%\%%~nF.lock"
    type nul > "!LOCK!" 2>nul

    rem バックグラウンドでジョブ起動
    rem ジョブ終了時にロックファイルを削除してスロット解放
    start "" /b cmd /c ^^
        "echo [START] %%~nxF & ^^
         type ""%%~fF"" > ""%OUT%\%%~nF.out"" & ^^
         echo [DONE ] %%~nxF & ^^
         del /q ""!LOCK!"" "

    set /a TOTAL+=1
    echo 投入[!TOTAL!]: %%~nxF
)

rem ==== 全ジョブ完了を待つ ====
call :wait_all
echo.
echo 全 %TOTAL% ジョブ完了
exit /b 0


rem ---- スロット取得サブルーチン ----
:acquire_slot
    set /a SLOTS_USED=0
    for %%L in ("%SLOTS%*.lock") do set /a SLOTS_USED+=1
    if !SLOTS_USED! geq %MAXJOBS% (
        timeout /t 1 /nobreak >nul
        goto :acquire_slot
    )
    exit /b 0


rem ---- 全ジョブ完了待ちサブルーチン ----
:wait_all
    dir /b "%SLOTS%*.lock" >nul 2>&1
    if not errorlevel 1 (
        timeout /t 1 /nobreak >nul
        goto :wait_all
    )
    exit /b 0


endlocal

方法3: ジョブ別ログ管理(stdout・stderr・終了コード)

並列処理では標準出力が混在して読みにくくなります。各ジョブの出力をファイルに分けて記録することで、問題発生時にどのジョブが失敗したか特定しやすくなります。

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

set "SRC=.in"
set "OUT=.out"
set MAXJOBS=4
set "SLOTS=%TEMPat-slots"
set "LOGDIR=%TEMPjoblogs"

if not exist "%OUT%"    mkdir "%OUT%"
if not exist "%SLOTS%"  mkdir "%SLOTS%"
if not exist "%LOGDIR%" mkdir "%LOGDIR%"

set TOTAL=0

for %%F in ("%SRC%*.txt") do (
    call :acquire_slot
    set "LOCK=%SLOTS%\%%~nF.lock"
    type nul > "!LOCK!" 2>nul

    rem ジョブの stdout / stderr / 終了コードをそれぞれ別ファイルに記録
    start "" /b cmd /c ^^
        "(type ""%%~fF"" > ""%OUT%\%%~nF.out"") ^^
         > ""%LOGDIR%\%%~nF.log"" ^^
         2> ""%LOGDIR%\%%~nF.err"" ^^
         & echo !ERRORLEVEL! > ""%LOGDIR%\%%~nF.code"" ^^
         & del /q ""!LOCK!"" "

    set /a TOTAL+=1
)

call :wait_all

rem ==== 結果集計 ====
echo.
echo === 処理結果 ===
set OK=0
set NG=0
for %%C in ("%LOGDIR%*.code") do (
    set /p CODE=<"%%~fC"
    if "!CODE!"=="0" (
        set /a OK+=1
    ) else (
        echo [FAIL] %%~nC (終了コード: !CODE!)
        set /a NG+=1
    )
)
echo 成功: !OK! 件 / 失敗: !NG! 件

exit /b 0

:acquire_slot
    set /a SLOTS_USED=0
    for %%L in ("%SLOTS%*.lock") do set /a SLOTS_USED+=1
    if !SLOTS_USED! geq %MAXJOBS% (
        timeout /t 1 /nobreak >nul
        goto :acquire_slot
    )
    exit /b 0

:wait_all
    dir /b "%SLOTS%*.lock" >nul 2>&1
    if not errorlevel 1 (
        timeout /t 1 /nobreak >nul
        goto :wait_all
    )
    exit /b 0

endlocal

方法4: 優先度と CPU コア割り当てで負荷を制御

重い並列処理は PC の通常操作に影響します。優先度を下げたり使用 CPU コアを限定することで影響を最小化できます。

優先度を下げて実行(/low / /belownormal)

rem /low: アイドル優先度(PC への影響を最小限に)
start "" /b /low cmd /c "heavy_job.bat"

rem /belownormal: 通常より少し低い優先度
start "" /b /belownormal cmd /c "heavy_job.bat"

rem スロット制御と組み合わせ(低優先度で4並列)
start "" /b /low cmd /c ^^
    "(process.exe -i ""input.txt"") & del /q ""slot.lock"""

CPU コアを限定する(/affinity)

rem /affinity の値は使用する CPU コアの16進数ビットマスク
rem コア0のみ: /affinity 1 (=0x01)
rem コア0,1:   /affinity 3 (=0x03)
rem コア0〜3:  /affinity F (=0x0F)
rem コア0,2,4: /affinity 15(=0x15)

rem 4コアPCでコア0,1のみ使用(コア2,3は他の作業に残す)
start "" /b /affinity 3 cmd /c "convert.exe input.dat"

rem 低優先度 + コア制限の組み合わせ
start "" /b /low /affinity 3 cmd /c "heavy_job.bat"
コア数 /affinity 値 16進
コア0のみ 1 0x01
コア0〜1 3 0x03
コア0〜3 15 0x0F
コア0〜7 255 0xFF

実践例A: 大量ファイルを並列変換する完全版

スロット制御・ジョブ別ログ・結果集計・古いスロットクリアをすべて含む本番運用向け完全版です。

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

rem ==== 設定 ====
set "SRC=.in"
set "OUT=.out"
set MAXJOBS=4
set "SLOTS=%TEMP%slots_%RANDOM%"
set "LOGDIR=%TEMP%logs_%RANDOM%"

mkdir "%OUT%"    2>nul
mkdir "%SLOTS%"  2>nul
mkdir "%LOGDIR%" 2>nul

echo ====================================
echo  並列処理開始: %DATE% %TIME%
echo  最大並列数: %MAXJOBS%
echo ====================================
echo.

set TOTAL=0

for %%F in ("%SRC%*.txt") do (
    call :acquire_slot
    set "LOCK=%SLOTS%\%%~nF.lock"
    type nul > "!LOCK!" 2>nul

    start "" /b /belownormal cmd /c ^^
        "(type ""%%~fF"" > ""%OUT%\%%~nF.out"") ^^
         > ""%LOGDIR%\%%~nF.log"" ^^
         2> ""%LOGDIR%\%%~nF.err"" ^^
         & echo !ERRORLEVEL! > ""%LOGDIR%\%%~nF.code"" ^^
         & del /q ""!LOCK!"""

    set /a TOTAL+=1
    set /a SLOTS_USED=0
    for %%L in ("%SLOTS%*.lock") do set /a SLOTS_USED+=1
    echo [!TOTAL!] 投入: %%~nxF (並列数: !SLOTS_USED!/%MAXJOBS%)
)

echo.
echo 全ジョブ投入完了。完了を待機中...
call :wait_all

rem ==== 結果集計 ====
echo.
echo === 処理結果 ===
set OK=0
set NG=0

for %%C in ("%LOGDIR%*.code") do (
    set /p CODE=<"%%~fC"
    if "!CODE!"=="0" (
        set /a OK+=1
    ) else (
        echo [FAIL] %%~nC
        set /a NG+=1
        rem エラーログがあれば表示
        if exist "%LOGDIR%\%%~nC.err" (
            type "%LOGDIR%\%%~nC.err"
        )
    )
)

echo.
echo ====================================
echo  終了: %DATE% %TIME%
echo  合計: %TOTAL% 件  成功: !OK!  失敗: !NG!
echo ====================================

rem ==== クリーンアップ ====
rd /s /q "%SLOTS%"  2>nul

if !NG! gtr 0 exit /b 1
exit /b 0


:acquire_slot
    set /a SLOTS_USED=0
    for %%L in ("%SLOTS%*.lock") do set /a SLOTS_USED+=1
    if !SLOTS_USED! geq %MAXJOBS% (
        timeout /t 1 /nobreak >nul
        goto :acquire_slot
    )
    exit /b 0

:wait_all
    dir /b "%SLOTS%*.lock" >nul 2>&1
    if not errorlevel 1 (
        timeout /t 1 /nobreak >nul
        goto :wait_all
    )
    exit /b 0

endlocal

実践例B: 複数ホストへの並列疎通確認

複数サーバーへ ping を並列で送信し、結果を集計するパターンです。逐次だと N×1秒かかるところが、並列では約1秒で完了します。

@echo off
setlocal enabledelayedexpansion

rem 確認対象ホストリスト
set "HOSTS=192.168.1.1 192.168.1.2 192.168.1.3 192.168.1.4 192.168.1.5"

set "SLOTS=%TEMP%ping_slots"
set "LOGDIR=%TEMP%ping_logs"
mkdir "%SLOTS%"  2>nul
mkdir "%LOGDIR%" 2>nul

echo 疎通確認開始: %DATE% %TIME%

rem 全ホストを並列で ping(上限8並列)
set MAXJOBS=8

for %%H in (%HOSTS%) do (
    call :acquire_slot
    set "LOCK=%SLOTS%\%%H.lock"
    type nul > "!LOCK!" 2>nul

    start "" /b cmd /c ^^
        "ping -n 1 %%H >nul 2>&1 ^^
         & echo !ERRORLEVEL! > ""%LOGDIR%\%%H.code"" ^^
         & del /q ""!LOCK!"""
)

call :wait_all

rem 結果表示
echo.
echo === 疎通確認結果 ===
for %%H in (%HOSTS%) do (
    if exist "%LOGDIR%\%%H.code" (
        set /p CODE=<"%LOGDIR%\%%H.code"
        if "!CODE!"=="0" (
            echo [  OK ] %%H
        ) else (
            echo [FAIL ] %%H
        )
    )
)

rd /s /q "%SLOTS%"  2>nul
rd /s /q "%LOGDIR%" 2>nul

exit /b 0

:acquire_slot
    set /a SLOTS_USED=0
    for %%L in ("%SLOTS%*.lock") do set /a SLOTS_USED+=1
    if !SLOTS_USED! geq %MAXJOBS% (
        timeout /t 1 /nobreak >nul
        goto :acquire_slot
    )
    exit /b 0

:wait_all
    dir /b "%SLOTS%*.lock" >nul 2>&1
    if not errorlevel 1 (
        timeout /t 1 /nobreak >nul
        goto :wait_all
    )
    exit /b 0

endlocal

よくある落とし穴

落とし穴1: start /wait は並列処理ではない

rem NG: start /wait は起動したプロセスが終わるまで待つ → 逐次処理と同じ
for %%F in (*.txt) do (
    start "" /wait cmd /c "process.exe %%F"
)

rem OK: /b で並列起動し、スロット制御で同時数を管理する
for %%F in (*.txt) do (
    call :acquire_slot
    start "" /b cmd /c "process.exe %%F & del lock.tmp"
)

落とし穴2: ウィンドウタイトルを省略すると意図しない動作になる

rem NG: タイトルを省略すると、コマンドの先頭語がタイトルとして解釈される場合がある
start /b cmd /c "myapp.exe input.txt"

rem OK: タイトルを空文字 "" で明示する
start "" /b cmd /c "myapp.exe input.txt"

落とし穴3: ロックファイルの残存でスロットが詰まる

rem ジョブが強制終了するとロックファイルが削除されず残り続ける
rem → 次回実行時にスロットが埋まったままになる

rem 対策1: 実行前にスロットフォルダを再作成してクリア
rd /s /q "%SLOTS%" 2>nul
mkdir "%SLOTS%"

rem 対策2: スロットフォルダに %RANDOM% をランダムサフィックスで一意化
set "SLOTS=%TEMP%at-slots-%RANDOM%"

落とし穴4: enabledelayedexpansion なしで !VAR! が展開されない

rem NG: setlocal のみだと !LOCK! が展開されず空のパスになる
setlocal
set "LOCK=%SLOTS%\%%~nF.lock"
type nul > "!LOCK!"   ← !LOCK! が空になってエラー

rem OK: setlocal enabledelayedexpansion を使う
setlocal enabledelayedexpansion
set "LOCK=%SLOTS%\%%~nF.lock"
type nul > "!LOCK!"   ← 正しく展開される

落とし穴5: 相対パスが子プロセスで変わる

rem start で起動した子プロセスの作業ディレクトリはバッチと同じとは限らない

rem 対策: スクリプト冒頭で cd /d "%~dp0" を実行して作業ディレクトリを固定
@echo off
cd /d "%~dp0"

rem さらに子プロセスへ渡す引数もフルパスを使う
for %%F in ("%~dp0in*.txt") do (
    start "" /b cmd /c "process.exe ""%%~fF"""
)

よくある質問(FAQ)

Q MAXJOBS の最適値はどう決めれば良いか
A

処理の種類によって異なります。

処理タイプ 推奨 MAXJOBS
CPU バウンド(圧縮・変換など) 物理 CPU コア数(例: 4コアなら 4)
I/O バウンド(ファイルコピー・ネットワーク) CPU コア数 × 2〜4 程度
ネットワーク(ping・HTTP) 10〜20 程度でも可

まず MAXJOBS=4 で試し、タスクマネージャーで CPU / ディスク使用率を見ながら増減してください。

Q 処理の進捗を確認したい
A

完了した .code ファイルの件数を数えることで進捗がわかります。

setlocal enabledelayedexpansion
rem 進捗確認(別のコマンドプロンプトから実行)
set /a DONE=0
for %%C in ("%LOGDIR%*.code") do set /a DONE+=1
echo 完了: !DONE! 件
Q 失敗したジョブだけ再実行したい
A

.code ファイルを読んで終了コードが 0 以外のファイルだけ再投入します。

setlocal enabledelayedexpansion
rem 失敗ジョブの再実行
for %%C in ("%LOGDIR%*.code") do (
    set /p CODE=<"%%~fC"
    if not "!CODE!"=="0" (
        echo 再実行: %%~nC
        rem ここで再投入処理を記述
    )
)
Q 並列処理の実行時間を計測したい
A

開始・終了時刻を記録して差分を表示します。

@echo off
for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set DT=%%I
set START_TIME=%DT:~8,2%:%DT:~10,2%:%DT:~12,2%

rem ... 並列処理 ...

for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set DT=%%I
set END_TIME=%DT:~8,2%:%DT:~10,2%:%DT:~12,2%

echo 開始: %START_TIME%
echo 終了: %END_TIME%
Q PowerShell の方が並列処理は簡単か
A

PowerShell 7 以降の ForEach-Object -Parallel を使うと、スロット制御込みで1行で並列処理が書けます。ただしインストールが必要です。
Windows 標準のバッチのみで外部ツール不要にこだわる場合は本記事のパターンを使ってください。

rem PowerShell 7+ での並列処理(比較参考)
rem Get-ChildItem *.txt | ForEach-Object -Parallel { ... } -ThrottleLimit 4

まとめ

目的 推奨パターン
少数ファイルを並列起動 start "" /b cmd /c "..." を並べる
大量ファイルの並列処理 スロット制御(ロックファイル + :acquire_slot
全ジョブ完了を待つ :wait_all(ロック数がゼロになるまで1秒ポーリング)
各ジョブのログを記録 > job.log 2> job.err & echo %ERRORLEVEL% > job.code
PC への影響を抑える /low(低優先度)または /belownormal
使用 CPU コアを限定 /affinity N(16進ビットマスク)
スロット残存対策 起動前に rd /s /q %SLOTS% でクリア

処理の一時停止・待機については 処理を一時停止する方法完全ガイド を、ファイルのコピー操作については 特定の文字列を含むファイルをコピーする方法 も合わせて参照してください。