Bash 原生不支援多線程,但透過命名管道(FIFO)+ 文件描述符 + 後台進程可實現高效並發控制模型。本文深度解析經典「令牌桶」模式實現,適用於 SSH 批量檢查、Ping 掃描、API 並發請求等 I/O 密集型任務,效能提升可達線程數倍數。
┌─────────────┐ 寫入令牌 ┌─────────────┐
│ 主進程 │ ──────────────→ │ FIFO 管道 │ ← 固定容量 = 最大並發數
│ (生產者) │ │ (緩衝區) │
└─────────────┘ └──────┬──────┘
│ 讀取令牌 (阻塞)
▼
┌─────────────────┐
│ 工作進程池 │
│ (消費者/後台) │
│ 讀取→執行→歸還 │
└─────────────────┘
#!/bin/bash
# multithread.sh - 通用並發控制模板
# === 配置區 ===
MAX_THREADS=15 # 最大並發數
TASK_COUNT=50 # 總任務數
TASK_FUNC="worker" # 工作函數名
# === 工作函數(替換為實際業務邏輯)===
worker() {
local task_id=$1
sleep 3 # 模擬耗時操作(SSH、curl、ping 等)
echo "Task $task_id completed"
return 0
}
# === 核心並發引擎 ===
fifo_file="/tmp/$$.fifo"
mkfifo "$fifo_file"
exec 6<>"$fifo_file" # 雙向綁定 fd6,關鍵:非阻塞寫入
rm "$fifo_file" # 立即刪除檔案,fd6 仍持有引用
# 初始化令牌池
for ((i=0; i&6 # 寫入 MAX_THREADS 個換行符作為令牌
done
# 任務分發循環
for ((i=1; i<=TASK_COUNT; i++)); do
read -u6 # 讀取令牌(無令牌則阻塞等待)
{
worker "$i" # 執行任務
ret=$?
if ((ret == 0)); then
echo "Task $i succeeded" >&2
else
echo "Task $i failed" >&2
fi
echo >&6 # 歸還令牌
} &
done
wait # 等待所有後台進程結束
exec 6>&- # 關閉 fd6
echo "All $TASK_COUNT tasks completed with $MAX_THREADS concurrent workers"
exec 6<>file 雙向綁定的必要性< 唯讀、> 唯寫、<> 讀寫雙向exec 6>file 寫入時會阻塞,直到有讀取者rm $fifo_file 後 fd6 仍有效Linux 檔案系統:目錄項刪除不影響已打開的文件描述符,管道在記憶體中持續存在直到所有 fd 關閉。這確保管道僅在程式生命週期內存在,不留殘留檔案。
初始:FIFO 中有 15 個 \n (MAX_THREADS=15)
循環 1-15:read -u6 即時成功,啟動 15 個後台進程
循環 16:read -u6 阻塞(FIFO 空)
→ 某進程結束 → echo >&6 歸還令牌 → read 繼續 → 啟動第 16 個
...
| 模式 | 總耗時公式 | 範例 (50任務/3秒/15並發) |
|---|---|---|
| 串行 | T × N | 3 × 50 = 150 秒 |
| 並發 | T × ceil(N/C) | 3 × ceil(50/15) = 3 × 4 = 12 秒 |
| 加速比 | C (理想值) | 12.5 倍 |

ssh_exec() {
local host=$1
ssh -o ConnectTimeout=5 -o BatchMode=yes "$host" "uptime"
}
# 任務列表產生
hosts=(server{1..100}.example.com)
TASK_COUNT=${#hosts[@]}
# 修改 worker 函數
worker() {
local idx=$1
ssh_exec "${hosts[idx-1]}"
}
check_url() {
local url=$1
curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url"
}
urls=( $(cat url_list.txt) )
TASK_COUNT=${#urls[@]}
worker() {
local idx=$1
local code=$(check_url "${urls[idx-1]}")
[[ $code == 200 ]] && echo "OK: ${urls[idx-1]}" || echo "FAIL[$code]: ${urls[idx-1]}"
}
dirs=(/data/project_{1..20})
worker() {
local idx=$1
tar -czf "/backup/${dirs[idx-1]##*/}_$(date +%F).tar.gz" "${dirs[idx-1]}"
}
# 結果收集陣列(需 Bash 4.3+ 關聯陣列)
declare -A results
worker() {
local idx=$1
local output
output=$(do_work "$idx" 2>&1)
local ret=$?
# 線程安全寫入(單一 echo 原子性)
echo "RESULT:$idx:$ret:$output" >&7
} 7>>/tmp/results.$$.log
# 結果收集進程
collector() {
while IFS=: read -r _ idx ret output; do
results[$idx]="$ret|$output"
done < /tmp/results.$$.log
}
| 陷阱 | 現象 | 解決 |
|---|---|---|
忘記 exec 6<> 用 > | 主進程寫入阻塞死鎖 | 必須雙向綁定 |
未 rm fifo | 殘留 /tmp/*.fifo 檔案 | 建立後立即刪除 |
未 wait | 主進程提前退出,孤兒進程 | 必須 wait 回收 |
| 變量作用域錯誤 | 後台子進程修改父進程變量無效 | 用檔案/管道/IPC 傳遞結果 |
| 並發數過大 | 系統資源耗盡(檔案描述符、記憶體、CPU) | 根據業務類型調整:I/O 密集 50-200,CPU 密集 = 核心數 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| FIFO 令牌桶 (本文) | 純 Bash、無依賴、精確控制、效能佳 | 代碼較繁瑣 | 腳本內並發、系統管理自動化 |
| GNU Parallel | 功能極強、自動負載均衡、進度條 | 需安裝、學習曲線 | 大規模數據處理、生產級批處理 |
| xargs -P | 簡潔、標準工具 | 難以控制精確並發、錯誤處理弱 | 簡單一次性任務 |
| Python multiprocessing | 真多進程、豐富同步原語、異常處理 | 啟動開銷大、需 Python 環境 | 複雜業務邏輯、長期維護專案 |
FIFO 令牌桶模型是 Bash 並發控制的黃金標準:純 Shell 實現、零外部依賴、精確控制並發度、效能接近理論最優。掌握此模式,讓 Shell 腳本突破串行瓶頸,在批量運維、資料處理、服務探測等場景發揮驚人威力。記住核心三步:建雙向管道、放令牌、讀令牌跑任務還令牌。