萬盛學電腦網

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

Linux Shell 多線程編程實戰:利用命名管道實現並發控制模型

Bash 原生不支援多線程,但透過命名管道(FIFO)+ 文件描述符 + 後台進程可實現高效並發控制模型。本文深度解析經典「令牌桶」模式實現,適用於 SSH 批量檢查、Ping 掃描、API 並發請求等 I/O 密集型任務,效能提升可達線程數倍數。

核心原理:FIFO 令牌桶模型

┌─────────────┐     寫入令牌      ┌─────────────┐
│  主進程      │ ──────────────→ │  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"

關鍵技術點深度解析

1. exec 6<>file 雙向綁定的必要性

  • < 唯讀、> 唯寫、<> 讀寫雙向
  • 單向 exec 6>file 寫入時會阻塞,直到有讀取者
  • 雙向綁定後,主進程可非阻塞寫入令牌,工作進程阻塞讀取

2. rm $fifo_file 後 fd6 仍有效

Linux 檔案系統:目錄項刪除不影響已打開的文件描述符,管道在記憶體中持續存在直到所有 fd 關閉。這確保管道僅在程式生命週期內存在,不留殘留檔案。

3. 令牌流轉機制

初始:FIFO 中有 15 個 \n (MAX_THREADS=15)
循環 1-15:read -u6 即時成功,啟動 15 個後台進程
循環 16:read -u6 阻塞(FIFO 空)
  → 某進程結束 → echo >&6 歸還令牌 → read 繼續 → 啟動第 16 個
...

效能數學模型

模式總耗時公式範例 (50任務/3秒/15並發)
串行T × N3 × 50 = 150 秒
並發T × ceil(N/C)3 × ceil(50/15) = 3 × 4 = 12 秒
加速比C (理想值)12.5 倍

多線程概念圖

實戰應用範例

1. SSH 批量執行命令

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]}"
}

2. URL 並發健康檢查

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]}"
}

3. 目錄並發壓縮備份

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 腳本突破串行瓶頸,在批量運維、資料處理、服務探測等場景發揮驚人威力。記住核心三步:建雙向管道、放令牌、讀令牌跑任務還令牌。

copyright © 萬盛學電腦網 all rights reserved