用 Bash 實作簡單的任務佇列 文章首圖

用 Bash 實作簡單的任務佇列

用 Bash 實作簡單的任務佇列

在 Linux 系統管理與自動化運算中,我們經常面臨一個常見的需求:如何有效地管理多個同時執行的背景任務?當你需要處理大量檔案、執行網路請求或進行批次運算時,若完全放任系統並行處理,可能會導致資源耗盡(如記憶體溢出或 CPU 飢餓);若完全串行執行,又會浪費寶貴的等待時間。

為了解決這個兩難,「任務佇列」(Task Queue)的概念便應運而生。今天我們將使用純 Bash 腳本,實作一個簡單但極具實用價值的任務佇列系統。這個方案不需要安裝 Redis 或 RabbitMQ 等複雜的外部依賴,僅依賴檔案系統與 wait 指令,即可在 Ubuntu 22.04 或 Debian 12 上輕鬆運行。

核心原理與設計思路

Bash 本身是一個強大的編程語言,具備處理背景程序(Background Processes)的能力。我們的核心邏輯如下:

  1. 佇列儲存:使用一個純文字檔案來儲存待處理的任務清單。
  2. 消費者模型:啟動固定數量的背景工作程序(Workers),每個 Worker 不斷從佇列檔案中讀取任務。
  3. 互斥機制:利用 flock 指令確保同一時間只有一個 Worker 能讀取並刪除任務,避免重複執行。
  4. 執行與回收:執行任務後,記錄結果並繼續等待下一個任務。

實作步驟

首先,請建立一個工作目錄並準備我們的腳本。我們將整個系統封存在一個腳本中,方便測試與部署。

#!/bin/bash

# 設定檔路徑
QUEUE_FILE="/tmp/task_queue.txt"
LOG_FILE="/tmp/worker.log"
MAX_WORKERS=3
LOCK_FILE="/tmp/task_queue.lock"

# 初始化佇列檔案
> "$QUEUE_FILE"
> "$LOG_FILE"

# 模擬耗時任務函數
process_task() {
    local task_id="$1"
    local task_cmd="$2"

    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Worker [$$] 開始執行任務: $task_id ($task_cmd)" >> "$LOG_FILE"

    # 執行實際指令
    eval "$task_cmd"

    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Worker [$$] 完成任務: $task_id" >> "$LOG_FILE"
}

# 主程式:啟動 Worker
start_workers() {
    echo "啟動 $MAX_WORKERS 個 Worker..."

    for ((i=1; i<=MAX_WORKERS; i++)); do
        (
            while true; do
                # 使用 flock 確保原子性讀取
                if flock -n 200; then
                    # 檢查佇列是否為空
                    if [ ! -s "$QUEUE_FILE" ]; then
                        flock -u 200
                        break # 佇列空了,結束 Worker
                    fi

                    # 讀取第一行並刪除它
                    task=$(head -n 1 "$QUEUE_FILE")
                    sed -i '1d' "$QUEUE_FILE"
                    flock -u 200

                    # 解析任務 (格式: ID|COMMAND)
                    task_id=$(echo "$task" | cut -d'|' -f1)
                    task_cmd=$(echo "$task" | cut -d'|' -f2-)

                    # 執行任務
                    process_task "$task_id" "$task_cmd"
                else
                    # 若無法獲得鎖,短暫休眠後重試
                    sleep 0.1
                fi
            done
        ) &
    done

    # 等待所有背景 Worker 結束
    wait
    echo "所有任務處理完畢!"
}

# 新增任務至佇列
add_task() {
    local task_id="$1"
    local task_cmd="$2"
    echo "${task_id}|${task_cmd}" >> "$QUEUE_FILE"
    echo "已新增任務: $task_id"
}

# --- 主執行區 ---

# 1. 新增一些測試任務
add_task "001" "sleep 2 && echo '任務 001 完成'"
add_task "002" "sleep 1 && echo '任務 002 完成'"
add_task "003" "sleep 3 && echo '任務 003 完成'"
add_task "004" "echo '任務 004 瞬間完成'"
add_task "005" "sleep 1 && echo '任務 005 完成'"

# 2. 啟動佇列處理
start_workers

# 3. 查看日誌
echo "--- 執行日誌 ---"
cat "$LOG_FILE"

# 4. 清理
rm -f "$QUEUE_FILE" "$LOG_FILE" "$LOCK_FILE"

將上述程式碼儲存為 task_queue.sh,並賦予執行權限 chmod +x task_queue.sh,然後執行 ./task_queue.sh。你會看到三個 Worker 並行處理這五個任務,總執行時間將接近最長任務的時間(約 3-4 秒),而非串行執行的 7 秒以上。

常見問題與優化建議

1. 如何處理任務失敗或重試機制?

在上述範例中,若某個任務執行失敗,它不會被重新加入佇列。在生產環境中,建議在 process_task 函數中加入錯誤檢查。若 eval "$task_cmd" 回傳值非零,可將任務重新寫入佇列檔案(需再次獲取鎖),或寫入一個獨立的 failed_tasks.txt 以供後續人工介入或定時重試。

2. flock 的鎖釋放問題

我們的範例使用 flock -n 200 進行非阻塞鎖定。若腳本異常終止,鎖檔案可能會殘留。雖然 flock 在程序結束時會自動釋放鎖,但在極端情況下(如系統崩潰),建議定期清理 /tmp 下的鎖定檔案,或改用更嚴謹的鎖管理庫。此外,對於高併發場景,建議將佇列檔案從本地磁碟移至記憶體磁碟(如 tmpfs),以減少 I/O 瓶頸。

小結

透過 Bash 實作簡單的任務佇列,我們不僅能精確控制並行數量,避免伺服器資源過載,還能利用純原生工具實現複雜的自動化流程。這種方法特別適合中小規模的批次處理任務,或是作為微服務架構前的原型驗證。

雖然 Bash 並非為高併發設計,但結合 flock 與背景程序管理,它展現了 Linux 工具鏈的靈活與強大。下次當你面對一堆需要順序或有限並行處理的任務時,不妨試試這個輕量級的解決方案。