Strictly speaking Bash doesn't support Light-Weight Processes or threads. Nevertheless it is possible to implement a "thread pool"-like script without using any external tools. This can be useful if a high level language (i.e. Java, Python, Perl) is not available and the Bash interpreter is the only tool at hand.
To avoid confusion we'll use the term "pool of workers" instead of "thread pool". The following diagram illustrates the typical use case:
 
There is a process in charge of producing certain events or records, which can be processed in parallel. Let's call them "tasks". The tasks are inserted into a FIFO queue. There is a pool of workers monitoring the FIFO qoeue. Whenever a worker becomes idle and there is a task pending in the queue the task is dequeued and processed by the spare worker. If all workers are busy the tasks will accumulate in the queue.
Let's split the implementation into 2 components: the worker and the pool.
Worker
The worker contains the code, which will be executed in parallel. This script is not intended to be executed by the user directly. So, let's avoid the file extension ".sh" and the classical "#!/bin/bash" header. The script name can be just worker.
# The first command line argument is a unique ID:
WORKER_ID=$1
# Simple logger
log(){
  printf "`date '+%H:%M:%S'` [Worker#%s] %s\n" "$WORKER_ID" "$1" >>worker.log
}
# Wait for the queue
while [ ! -e myqueue ]
do
  sleep 1
done
touch mylock
exec 3<myqueue # FD3 <- queue
exec 4<mylock  # FD4 <- lock
while true
do
  # Read the next task from the queue
  flock 4
  IFS= read -r -u 3 task
  flock -u 4
  if [ -z "$task" ]
  then
    sleep 1
    continue
  fi
  log "Processing task: ${task}"
done
Note that the worker uses a file to synchronize the access to the FIFO queue. The file (mylock) is locked before reading the queue and unlocked after reading the queue. The synchronization is atomic. Therefore, only a single worker can perform a dequeue at a time. This is important to avoid concurrency issues.
In Bash 4 and later it is possible to optimize the logger to avoid starting a subprocess:
log(){
  printf '%(%Y-%m-%d %H:%M:%S)T [Worker#%s] %s\n' '-1' "$WORKER_ID" "$1" >>worker.log
}
Pool
The pool is another script, which will start, stop and monitor the pool of workers. This is pool.sh:
#!/bin/bash
# The pool of workers size:
WORKERS=3
# Check the pool status
status(){
  alive=0
  for p in pid.*
  do
    [ "$p" = 'pid.*' ] && break
    pid="${p:4}"
    wk=`ps -fp "$pid" 2>/dev/null | sed -n 's/.* worker //p'`
    if [ ! -z "$wk" ]
    then
      let "alive++"
      [ $1 = 0 ] && printf 'Worker %s is running, PID %s\n' "$wk" "$pid"
    else
      rm -f "$p"
    fi
  done
  if [ $1 = 0 ]
  then
    [ $alive = 0 ] && printf 'NOK\n' || printf 'OK: %s/%s\n' $alive "$WORKERS"
  fi
  return $alive
}
# Stop the pool
stop(){
  for p in pid.*
  do
    [ "$p" = 'pid.*' ] && break
    pid="${p:4}"
    wk=`ps -fp "$pid" 2>/dev/null | sed -n 's/.* worker //p'`
    if [ ! -z "$wk" ]
    then
      kill "$pid" 2>/dev/null
      sleep 0
      kill -0 "$pid" 2>/dev/null && sleep 1 && kill -9 "$pid" 2>/dev/null
    fi
    rm -f "$p"
  done
}
# Start the pool
run(){
  status 1
  [ $? != 0 ] && printf 'Already running\n' && exit 0
  # Setup the queue
  rm -f myqueue mylock
  mkfifo myqueue
  # Launch N workers in parallel
  for i in `seq $WORKERS`
  do
    /bin/bash worker $i &
    touch pid.$!
  done
}
case $1 in
  "start")
    run
    ;;
  "stop")
    stop
    ;;
  "status")
    status 0
    ;;
  *)
    printf 'Unsupported command\n'
    ;;
esac
Let's start the pool of workers:
test@celersms:~/test$ bash pool.sh start test@celersms:~/test$ bash pool.sh status Worker 1 is running, PID 4454 Worker 2 is running, PID 4457 Worker 3 is running, PID 4460 OK: 3/3
There are 3 workers. As you can see with "ps", each worker is actually a subprocess:
test@celersms:~/test$ ps -fu | grep worker test 4460 0.0 0.1 5292 1288 pts/10 S 22:27 0:00 /bin/bash worker 3 test 4457 0.0 0.1 5292 1288 pts/10 S 22:27 0:00 /bin/bash worker 2 test 4454 0.0 0.1 5292 1288 pts/10 S 22:27 0:00 /bin/bash worker 1
We can stop the pool of workers anytime:
test@celersms:~/test$ bash pool.sh stop test@celersms:~/test$ bash pool.sh status NOK test@celersms:~/test$ ps -fu | grep worker test@celersms:~/test$
In order to increase or decrease the number of workers just modify the value of the WORKERS variable inside pool.sh. Let's start the pool again. After that we can queue a dummy list of tasks by just writing into myqueue:
test@celersms:~/test$ bash pool.sh start test@celersms:~/test$ cat >>myqueue <<EOF > task1 > task2 > task3 > task4 > task5 > task6 > task7 > task8 > task9 > EOF test@celersms:~/test$ cat worker.log 22:40:26 [Worker#3] Processing task: task1 22:40:26 [Worker#2] Processing task: task2 22:40:26 [Worker#1] Processing task: task3 22:40:26 [Worker#3] Processing task: task4 22:40:26 [Worker#1] Processing task: task5 22:40:26 [Worker#2] Processing task: task6 22:40:26 [Worker#3] Processing task: task7 22:40:26 [Worker#1] Processing task: task8 22:40:26 [Worker#2] Processing task: task9
The dummy tasks were processed asynchronously. Each worker was executed in a random order, but the overall workload was distributed evenly.
Sample project
 A complete sample project
containing the Bash scripts is available in our GitHub repository.
The code was tested in Bash V3.2.25 and later.
A complete sample project
containing the Bash scripts is available in our GitHub repository.
The code was tested in Bash V3.2.25 and later.
 

