跳至主要內容

雲端 Mac 上的 iOS 穩定優先佇列驗收

雲端 Mac 上的 iOS 穩定優先佇列驗收

雲端 Mac 上的 iOS 穩定優先佇列驗收

圖片預先擷取、待上傳項目與本機任務排序,經常會用到優先佇列。在開發機上連續加入三筆任務,再依序取出,很容易得到正確結果;但到了雲端 Mac 的迴歸測試,加入與取出交錯執行時,同優先級的任務卻開始互換位置。這裡不建置完整的排程器,而是將 Swift 二元堆積獨立出來,驗證它是否始終遵守排序契約。

先定義優先級與穩定性的意義

本文約定:優先級數字越小,越先出列;數字相同時,先入列的先出列。任務識別碼只是業務資料,不參與比較。同一個任務再次入列時,視為新的項目,並取得新的入列序號。

先將這幾條規則寫成測試規格,再實作資料結構。不要拿陣列索引當作永久序號:出列與交換都會改變索引,無法藉此保證穩定性。也不要透過優先級相減來判斷大小,極端整數值可能造成溢位。

穩定性不等於公平性。高優先級任務持續進來時,低優先級任務仍可能長期等待;老化與配額屬於排程策略,不應悄悄混進堆積的比較函式。

以下實作是實值型別,僅供循序存取。整合至並行程式時,應透過單一隔離邊界統一管理,不可讓多個任務同時修改同一個實例。

實作帶有入列序號的最小堆積

將程式碼儲存為 main.swift。比較鍵由優先級與遞增序號組成,入列時的上浮與出列時的下沉都必須使用同一個比較函式,避免兩條路徑對「誰排在前面」有不同的判斷。

struct StableQueue<Value> {
    enum Failure: Error {
        case sequenceExhausted
    }

    private struct Entry {
        let value: Value
        let priority: Int
        let sequence: UInt64
    }

    private var entries: [Entry] = []
    private var nextSequence: UInt64 = 0

    private func precedes(_ a: Entry, _ b: Entry) -> Bool {
        if a.priority != b.priority {
            return a.priority < b.priority
        }
        return a.sequence < b.sequence
    }

    mutating func enqueue(_ value: Value, priority: Int) throws {
        guard nextSequence < UInt64.max else {
            throw Failure.sequenceExhausted
        }
        entries.append(Entry(
            value: value,
            priority: priority,
            sequence: nextSequence
        ))
        nextSequence += 1

        var child = entries.count - 1
        while child > 0 {
            let parent = (child - 1) / 2
            guard precedes(entries[child], entries[parent]) else {
                break
            }
            entries.swapAt(child, parent)
            child = parent
        }
    }

    mutating func dequeue() -> Value? {
        guard !entries.isEmpty else { return nil }
        if entries.count == 1 {
            return entries.removeLast().value
        }

        let result = entries[0].value
        entries[0] = entries.removeLast()
        var parent = 0

        while parent < entries.count / 2 {
            let left = parent * 2 + 1
            let right = left + 1
            var candidate = left
            if right < entries.count,
               precedes(entries[right], entries[left]) {
                candidate = right
            }
            guard precedes(entries[candidate], entries[parent]) else {
                break
            }
            entries.swapAt(parent, candidate)
            parent = candidate
        }
        return result
    }
}

序號到達上限時,明確拋出錯誤,不讓計數器回繞,也不在佇列清空後悄悄歸零。這個實作保留最大值不用,讓失敗發生在修改陣列之前;測試應確認,入列失敗不會改變既有項目的順序。

用交錯操作抓出下沉錯誤

將下列片段接在同一個檔案末尾。它不只檢查最終排序,也驗證取出一筆後再加入同優先級任務時,舊項目仍應排在新項目前面。

func requireEqual<T: Equatable>(_ actual: T, _ expected: T) {
    precondition(actual == expected, "Queue contract failed")
}

var queue = StableQueue<String>()
requireEqual(queue.dequeue(), Optional<String>.none)

try queue.enqueue("A", priority: 2)
try queue.enqueue("B", priority: 1)
try queue.enqueue("C", priority: 2)
requireEqual(queue.dequeue(), Optional("B"))

try queue.enqueue("D", priority: 2)
try queue.enqueue("E", priority: Int.min)
requireEqual(queue.dequeue(), Optional("E"))
requireEqual(queue.dequeue(), Optional("A"))
requireEqual(queue.dequeue(), Optional("C"))
requireEqual(queue.dequeue(), Optional("D"))
requireEqual(queue.dequeue(), Optional<String>.none)

在雲端 Mac 的工作目錄執行:

set -eu
mkdir -p .build
xcrun swiftc -Onone main.swift -o .build/queue-check
.build/queue-check

成功時不會有任何輸出,失敗則以非零結束碼中止作業。這裡不使用 assert,避免檢查在最佳化建置中被移除;也不能拿「編譯成功」取代實際執行驗收。

再補上三組確定性案例:全部同優先級時維持輸入順序;依優先級降冪全部入列後,以升冪順序取出;只有兩個元素時先取出一個,再持續交錯加入與取出。最後一組特別容易暴露右子節點不存在時的越界存取問題。

用參考模型擴大涵蓋範圍

手寫案例能說明契約,卻難以涵蓋所有堆積形狀。參考模型可以直接用陣列儲存「任務編號、優先級、入列序號」,每次取出前依後兩項排序。雖然速度較慢,但實作簡單,適合當作小規模測試的判定基準。

產生交錯操作時,每次入列都同步寫入模型;每次出列則比較兩邊回傳的可選任務編號,操作結束後再將兩邊全部取空。只比較優先級並不足夠,因為同優先級任務交換順序也算缺陷。

使用可指定種子的偽隨機產生器,不要直接依賴無法重現的隨機來源。失敗紀錄至少要包含種子、操作索引、完整的操作前綴,以及兩邊的結果。定位問題時,從失敗前綴中逐段刪除操作,保留仍會失敗的最短序列。

確保參考模型獨立於實作

參考模型不要重用堆積的比較函式,否則同一個錯誤可能讓兩邊同時通過。模型可以先比較優先級,只有相等時才選擇最早入列的項目,直接表達業務規則。

納入迴歸測試並限定結論

提交前固定執行空佇列、單一元素、同優先級穩定性、整數極值與交錯操作測試,再執行一批固定種子的模型比對。新增的失敗種子應納入長期迴歸測試集,不要只是增加隨機測試次數後重新執行。

序號耗盡無法靠一般的入列次數來涵蓋。正式模組可以提供僅供測試使用的內部初始化介面,將計數器設在接近上限的位置,檢查最後一次允許入列與下一次拒絕入列的邊界,避免開放可修改正式環境狀態的公開介面。

封存 Swift 工具鏈版本、提交識別碼與失敗操作序列即可,不必保留業務負載資料。最終驗收結論應明確指出:這裡驗證的是循序存取佇列的順序與邊界行為,不包含執行緒安全、任務執行成功率或排程公平性。將這些邊界寫清楚,後續整合執行器時,才不會把資料結構測試誤當成整個系統的保證。

常見問題

二元堆積會自動保證同優先級先進先出嗎?

不會。比較鍵必須先比較優先級,再比較插入序號;每次重新加入佇列,都應取得新序號。

穩定優先佇列能避免低優先級工作一直等待嗎?

不能。穩定性只規範同優先級項目的順序;避免飢餓需要另外設計老化、配額或分層排程政策。

依照專案進度租用 Mac mini

選擇獨享實體節點,提供日租、週租、月租和季租。旺季可加租;租期結束後若未續租,服務即會停止。實際可用狀態請以控制台即時回報為準。

讓下一次建置有台機器可用

先確認規格與租期,再為專案選擇合適的獨享實體節點。