I added the necessary management to SET /P Batch commands in order to use they in the event handlers of previous example, so the whole multi-thread application can be written in pure Batch. The details about SET /P problems with pipes and the method to solve they are described at
Set /P problems with pipes topic.
Below there is an example program that show two different schemes for a multi-thread Batch file application: one with the main code at beginning of the pipe chain, and another one with the main code at end.
Code: Select all
@echo off
setlocal EnableDelayedExpansion
rem Multi-thread schemes in Batch files
rem Antonio Perez Ayala aka Aacini
set "myID=%~2"
if "%~1" neq "" goto %1
rem Define auxiliary variables
rem http://www.dostips.com/forum/viewtopic.php?f=3&t=6134
set LF=^
%Do not remove this line 1/2%
%Do not remove this line 2/2%
for /F %%a in ('copy /Z "%~F0" NUL') do set "CR=%%a"
set "spaces= "
for /L %%i in (1,1,10) do set "spaces=!spaces!!spaces!"
cd /D "%~DP0"
echo/
echo Example A: Main code send commands to a chain of waiting service threads
"%~NX0" MainA 3>&1 1>&2 | "%~NX0" ThreadA 1 | "%~NX0" ThreadA 2 | "%~NX0" ThreadA 3
echo/
echo Example B: Main code receive "completed" signals from active service threads
del request*.txt 2> NUL
(
start "" /B cmd /C "%~NX0" ThreadB 1
start "" /B cmd /C "%~NX0" ThreadB 2
) | "%~NX0" MainB
goto :EOF
:MainA
rem Example A: Main code send commands to a chain of waiting service threads
rem Use handle #3 to send commands to waiting threads
rem The "completed" signals may be received via files
rem The benefit of this scheme is that all threads stay in an efficient wait state
rem while waiting for requests from the main code
timeout /T 3 /NOBREAK > NUL
echo MainA sending a command to ThreadA #1
set "output=1: Command to ThreadA #1=first%spaces%"
echo %output:~0,1021%>&3
timeout /T 3 /NOBREAK > NUL
echo MainA sending a command to ThreadA #3
set "output=3: Command to ThreadA #3=third%spaces%"
echo %output:~0,1021%>&3
timeout /T 3 /NOBREAK > NUL
echo MainA sending a command to ThreadA #2
set "output=2: Command to ThreadA #2=second%spaces%"
echo %output:~0,1021%>&3
timeout /T 3 /NOBREAK > NUL
set "output=exit%spaces%"
set "output=%output:~0,1018%"
echo 3: %output%>&3
echo 2: %output%>&3
echo 1: %output%>&3
goto :EOF
:ThreadA
rem Get a command from main code or previous thread
set /P "command="
for /F "tokens=1-5" %%a in ("%command%") do (
if "%%a" equ "%myID%:" (
rem Command intended for this thread: execute it
echo ThreadA #%myID%, received command: "%%b %%c %%d %%e" > CON
) else (
rem Pass command to next thread in the chain
echo %command%
)
)
if "%command:~3,4%" neq "exit" goto ThreadA
echo ThreadA #%myID%, terminating... > CON
goto :EOF
:MainB
rem Example B: Main code receive "completed" signals from active service threads
rem The requests for active threads may be send via files
rem The "completed" signals are received via Stdin
rem The benefit of this scheme is that the main code stay in an efficient wait state
rem while waiting for the threads "completed" signals
ping -n 5 localhost > NUL
echo MainB placing a request for ThreadB #1
echo Request for ThreadB #1=first > request1.txt
echo MainB waiting for signal from ThreadB #1
set /P "signal="
echo MainB received signal: "%signal%"
echo exit > request1.txt
set /P "signal="
ping -n 5 localhost > NUL
echo MainB placing a request for ThreadB #2
echo Request for ThreadB #2=second > request2.txt
echo MainB waiting for signal from ThreadB #2
set /P "signal="
echo MainB received signal: "%signal%"
echo exit > request2.txt
set /P "signal="
goto :EOF
:ThreadB
rem Wait until a request appear
rem a delay command (ping or timeout) may be included here
if not exist request%myID%.txt goto ThreadB
set /P "request=" < request%myid%.txt
del request%myID%.txt
echo ThreadB #%myID%, received request: "%request%" > CON
ping -n 5 localhost > NUL
set "output=ThreadB #%myID% completed @%time%!CR!!LF!%spaces%"
rem Send the "completed" signal via Stdout
echo %output:~0,1021%
if "%request:~0,4%" neq "exit" goto ThreadB
echo ThreadB #%myID%, terminating... > CON
goto :EOF
Previous schemes have the disadvantage that the other part of the synchronization (the side not controlled by the pipe) must be achieved via the presence of a file or line that works like a
flag or semaphore; this flag must be tested in a GOTO assembled loop. A more efficient scheme can be assembled if the whole program start and end in a pipe, that is, the application may be divided in three parts: an Input part, a Thread part (that can be executed several concurrent times, one instance by each CPU processor core) and an Output part, all of they connected in a long pipeline. This scheme is very efficient, because the synchronization between all parts/threads is achieved by the operating system itself, not via a test executed in a loop.
Code: Select all
@echo off
setlocal EnableDelayedExpansion
rem Multi-thread schemes in Batch files
rem Antonio Perez Ayala aka Aacini
set "myID=%~2"
if "%~1" neq "" goto %1
rem Define auxiliary variables
rem http://www.dostips.com/forum/viewtopic.php?f=3&t=6134
set LF=^
%Do not remove this line 1/2%
%Do not remove this line 2/2%
for /F %%a in ('copy /Z "%~F0" NUL') do set "CR=%%a"
set "spaces= "
for /L %%i in (1,1,10) do set "spaces=!spaces!!spaces!"
rem Example C:
rem 1- The Input module get input from the user, that specify the task to perform
rem and distribute it between the concurrent threads.
rem 2- Each concurrent thread perform a part of the task and generate a partial result.
rem 3- The Output module group partial results from all threads
rem and output the complete result.
set "TRACE=>CON ECHO"
rem To activate trace, remove the next line
set "TRACE=REM"
rem Create the chain of threads linked by pipes
set "numThreads=%NUMBER_OF_PROCESSORS%"
set "threads="
for /L %%i in (%numThreads%,-1,1) do set "threads=!threads! | "%~NX0" Thread %%i"
echo Start of application
cd /D "%~DP0"
"%~NX0" Input | %threads:~3% | "%~NX0" Output
echo End of application
goto :EOF
:Input
%TRACE% INPUT: Start
echo/> CON
set /P "=Number of lines to generate (Enter to end): " > CON < NUL
:nextInput
rem Get next task from the user
set "number="
set /P "number=" > CON
if not defined number goto endInput
rem Divide the task by the number of threads
set /A chunk=number/numThreads, last=0
for /L %%i in (1,1,%numThreads%) do (
set /A "from%%i=last+1, to%%i=(last+=chunk)"
)
set "to%numThreads%=%number%"
rem Distribute the task between all threads
for /L %%i in (1,1,%numThreads%) do (
%TRACE% INPUT: Send "%%i !from%%i! !to%%i!"
set "output=%%i !from%%i! !to%%i!!CR!!LF!%spaces%"
echo !output:~0,1021!
)
goto nextInput
:endInput
rem Terminate the whole chain of threads (and Output)
%TRACE% INPUT: Send "exit"
set "output=exit!CR!!LF!%spaces%"
echo %output:~0,1021%
%TRACE% INPUT: End
goto :EOF
:Thread
%TRACE% #%myID%- THREAD: Start
set "output=result %myID%!CR!!LF!%spaces%"
:nextThread
rem Get next command from main code or previous thread
set /P "command="
%TRACE% #%myID%- THREAD: - Received "%command%"
for /F "tokens=1-3" %%a in ("%command%") do (
rem If the command is intended for this thread
if "%%a" equ "%myID%" (
rem Process it
%TRACE% #%myID%- THREAD: - - Working with %%b..%%c
(for /L %%i in (%%b,1,%%c) do (
echo Thread #%myID% @ !time! - Line %%i
)) > output%myID%.txt
%TRACE% #%myID%- THREAD: - - Sending result signal
echo %output:~0,1021%
) else (
rem Pass the command to next thread in chain
set "command=%command%!CR!!LF!%spaces%"
echo !command:~0,1021!
)
)
if "%command:~0,4%" neq "exit" goto nextThread
%TRACE% #%myID%- THREAD: End
goto :EOF
:Output
%TRACE% OUTPUT: Start
set /A resCount=0, resOrder=1
:nextOutput
rem Get next signal from thread chain
set /P "signal="
%TRACE% OUTPUT: Received "%signal%"
rem If the signal is a "result" one
if "%signal:~0,6%" equ "result" (
rem Show this partial result, if is the next one in output order;
rem this point avoids synchro problems beween threads
if "%signal:~7%" equ "%resOrder%" (
if "%TRACE%" equ "REM" type output%resOrder%.txt
set /A resOrder+=1
)
rem If all partial results were completed
set /A resCount+=1
if "!resCount!" equ "%numThreads%" (
rem Show previous results delayed by a synchro problem, if any
if "%TRACE%" equ "REM" for /L %%i in (!resOrder!,1,%numThreads%) do type output%%i.txt
rem Pass to next task
%TRACE% OUTPUT: Result complete
set /A resCount=0, resOrder=1
echo/
set /P "=Number of lines to generate (Enter to end): " < NUL
)
)
if "%signal%" neq "exit" goto nextOutput
%TRACE% OUTPUT: End
goto :EOF
The key point in this scheme is that any part/thread that is waiting in a SET /P command is placed in an
inactive state by the operating system, so it does not consume any CPU while is waiting. In traditional schemes, one CPU core is usually reserved for the control code; otherwise this code affects the performance of one of the concurrent threads because both parts share the same CPU core. In the event-driven pipe scheme,
all CPU cores can be used for the concurrent threads: the control code will be executed precisely until one of the concurrent threads ends, that is, when there is an available CPU core for it. In other words: the available CPU resources are used in the most efficient way!
Antonio