萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 作業系統 >  >> Linux 教學

Bash 多執行緒腳本實戰:從無序並發到可控線程池的進階技法

Bash 原生不支援執行緒,但透過後台作業(&)與命名管道即可實現並發控制。本文從最簡單的「全量並發」進階到「固定線程池」,並提供 Docker 容器啟動、錯誤處理、資源清理的生產級範本,助你在自動化運維、批次處理場景大幅縮短執行時間。

Bash 多線程腳本

一、入門:無序全量並發

利用 { ... } & 將任務丟入背景,wait 等待所有子進程結束。

#!/bin/bash
pairs=5
port=9080
port_m=27017

for ((i=1; i<=pairs; i++)); do
  mongo="mongo_00$i"
  web="acmeair_web_00$i"
  {
    docker run -d --name "$mongo" -p "$port_m:27017" mymongodb
    docker run -d --name "$web" -p "$port:9080" --link "$mongo:mongo" acmeair/web
  } &
  ((port++))
  ((port_m++))
done

wait   # 等待所有背景作業完成
echo "All containers started."

缺點: 並發數不可控,大量任務會耗盡 CPU/記憶體/檔案描述符,甚至觸發 OOM Killer。

二、進階:固定大小線程池(FIFO 信號量模式)

核心原理

  1. 建立 FIFO 檔案,綁定到檔案描述符(FD)6。
  2. 向 FD6 寫入 N 個換行符(Token),代表可用線程槽位。
  3. 每次啟動任務前 read -u6 取走一個 Token(槽位滿則阻塞)。
  4. 任務結束後 echo >&6 歸還 Token。

生產級範本

#!/bin/bash
set -euo pipefail

THREADS=15          # 最大並發數
TOTAL_TASKS=50      # 總任務數
FIFO_FILE="/tmp/$$.fifo"

# ---- 初始化信號量 ----
mkfifo "$FIFO_FILE"
exec 6<>"$FIFO_FILE"
rm -f "$FIFO_FILE"   # 刪除檔案實體,FD 仍有效

# 填充 Token
for ((i=0; i&6
done

# ---- 任務函數 ----
run_task() {
  local task_id=$1
  echo "[$(date '+%H:%M:%S')] Task $task_id START"
  
  # 實際業務邏輯(此處以 sleep 模擬)
  if docker run --rm alpine sleep 2; then
    echo "[$(date '+%H:%M:%S')] Task $task_id OK"
  else
    echo "[$(date '+%H:%M:%S')] Task $task_id FAILED" >&2
    return 1
  fi
}

# ---- 主迴圈 ----
for ((i=1; i<=TOTAL_TASKS; i++)); do
  read -u6          # 取 Token(阻塞直到有空槽)
  {
    run_task "$i"
    echo >&6        # 歸還 Token
  } &
done

wait                # 等待所有背景作業
exec 6>&-           # 關閉 FD
echo "All tasks completed."

關鍵點解析

語法作用
exec 6<>file雙向開啟 FD6,避免「讀端關閉導致寫端收到 SIGPIPE」
read -u6從 FD6 讀取一行(消耗一個 Token)
echo >&6向 FD6 寫入一行(歸還 Token)
{ ... } &將任務包裹為背景子 Shell,不阻塞主迴圈
set -euo pipefail嚴格模式:變數未設定/指令失敗/管道失敗即退出

三、進階變體:xargs 並行(無需手寫線程池)

GNU xargs -P 內建並行,適合「命令列參數化」的任務:

# 並行 15 執行 docker 啟動
seq 1 50 | xargs -P 15 -I {} bash -c '
  id={}
  docker run --rm --name "task_$id" alpine sleep 2
'

優勢: 程式碼極簡、自動負載均衡。劣勢: 複雜邏輯(錯誤處理、狀態收集)較難實現。

四、GNU Parallel:工業級並行利器

# 安裝:apt/yum/pacman install parallel
seq 1 50 | parallel -j 15 --eta --joblog /tmp/parallel.log \
  'docker run --rm --name task_{} alpine sleep 2'
  • --eta:顯示預估剩餘時間。
  • --joblog:記錄每任務狀態、耗時、退出碼。
  • --retries 2:失敗自動重試。

五、避坑指南與最佳實踐

  1. 資源配額: 以 ulimit -u 限制用戶進程數,防止 Fork 炸彈。
  2. 錯誤傳播: 子 Shell 失敗不會中斷主腳本,需自行收集退出碼(如寫入暫存檔、wait $pid 檢查 $?)。
  3. 清理陷阱: trap 'rm -f "$FIFO_FILE"; exec 6>&-; kill 0' EXIT INT TERM 確保異常退出時釋放資源。
  4. 日誌分離: 每任務輸出導向獨立檔案(> /tmp/task_$i.log 2>&1),避免標準輸出交錯。
  5. 信號量上限: 參考 nproc、記憶體、檔案描述符限制設定 THREADS,通常 2×CPU 核心數為宜。

結語

從 & + wait 到 FIFO 線程池,再到 xargs -P/GNU Parallel,Bash 並發技法各有甜點。簡單批次用 xargs,複雜流程控制用 FIFO 池,生產環境首選 GNU Parallel。掌握這些模式,讓你的自動化腳本在多核伺服器上跑出單核數十倍的吞吐量。

copyright © 萬盛學電腦網 all rights reserved