背景執行緒正在匯出四萬列的報表時,UI 執行緒設定了一個儲存格,結果落到磁碟上的檔案與任何曾經存在過的活頁簿都對不上。HotXLS 在 lxWorkbookView.pas 裡處理這一類 bug:IXLSWorkbookViewCore 在該處核發 O(1) 讀取租用與速敗寫入防護——租用開啟期間,每個變更進入點都會擲出例外,而不是逕行寫入
沒有堆疊追蹤就到來的失效
讀取活頁簿從來不是單一不可分割的操作。走一趟報表是散佈在數秒之內、數萬次個別的儲存格讀取,而一次 SetValue 落在其中兩次之間,就足以改變這趟走訪其餘部分看到的東西。傳統引擎把這點變得具體:TXLSCellRef.SetValue 可能呼叫 FSST.Remove 丟掉一個共用字串條目、重設 FValueType,並讓某個公式快取狀態失效——這一切發生的同時,另一個執行緒正在解參考的恰好就是這些結構。現場沒有任何東西崩潰。您得到的是一份小計加不起來的報表,或一個悄悄讀到「如今指向別處」之字串索引的匯出
HotXLS 刻意不靠「讓寫入者等待」來解決這件事。讀取者可能持有活頁簿好幾秒,而在 VCL 應用程式中,寫入者往往是主執行緒上的 UI 回呼或事件處理常式——把那個執行緒封鎖到背景匯出結束,是比讓編輯失敗更糟的結果。因此協調核心在「對開啟中的租用嘗試寫入」的當下、在任何欄位被碰觸之前,就擲出 EXLSWorkbookWriteGuardUnavailable,由呼叫端決定是要把編輯排入佇列、重試,還是告訴使用者。衝突要速敗,不要排隊
活頁簿從兩個執行緒讀取安全嗎?
安全——前提是兩個讀取者都持有租用,而且沒有人寫入。IXLSWorkbookViewCore.AcquireReadLease 取得一個 TCriticalSection、遞增計數器、對目前世代做快照,然後回傳一個 IXLSWorkbookReadLease——不論活頁簿裝著一千個還是一百萬個儲存格,都是常數時間。任意數量的租用可以共存,它們可以按任何順序釋放,而且每一個都透過自己的介面參考把核心釘住保活,所以租用比建立它的物件長命是安全的,而不是懸空指標。兩個引擎都參與:lxHandle.pas 中的 TXLSWorkbook 與 lxHandleX.pas 中的 TXLSXWorkbook 各自在建構子中建立一個核心,並暴露 _AcquireReadLease 與 _AcquireWriteGuard
同等重要的,是租用不替讀取路徑增加什麼。臨界區只涵蓋租用取得、租用釋放與寫入交易邊界——別無其他。普通的逐儲存格讀取從不進入鎖、監視器或原子計數器,所以持有租用對整趟掃描只花一次取得與一次釋放,而不是每個儲存格一次。這與平行 XLSX 剖析與記憶體配置器背後是同一種設計直覺:協調的成本付在邊界,絕不付在內圈迴圈。對稱的規則也成立——每當 WriteDepth 非零,AcquireReadLease 就擲出 EXLSWorkbookReadLeaseUnavailable,所以您不能從寫入交易內部開租用,連在寫入中的那個執行緒上都不行
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// 若有寫入進行中,擲出 EXLSWorkbookReadLeaseUnavailable
Lease := FWorkbook._AcquireReadLease;
Sheet := FWorkbook.Sheets[1];
Total := 0;
for Row := 1 to 50000 do
Total := Total + Sheet.Cells[Row, 3].Value;
FTotal := Total;
// 租用在此離開作用域:參考計數歸零,
// ReleaseReadLease 執行,寫入者再度成為可能
end;
寫入防護實際坐在哪裡?
坐在最低的可變動層,絕不坐在它之上的便利 API。_AcquireWriteGuard 是從 TXLSCellRef.SetValue 內部呼叫的,這意味著每條匯入它的公開路徑——Range.Value、工作表文字指定、逐儲存格複製、貼上——都被一次性把關,而不是每個包裝器各自重複一次「未來某個包裝器一定會忘記」的檢查。涵蓋範圍刻意很廣:在引入核心的那個批次中,lxHandle.pas 有 55 處防護取得,lxHandleX.pas 有 37 處
被把關的表面橫跨儲存格值與儲存格格式、TXLSWorkbook.Open、複製與貼上、定義名稱(Add、重新命名、RefersTo、Visible、IsMacro、Comment、Delete)、工作表詮釋資料如 Name、Zoom、Visible、StandardHeight、FreezePanes、Protect 與 Activate、版面設定、分頁線,以及 Calculate。擺放位置就是重點:防護在第一個欄位被寫入之前取得,而不是事後由通知攔截勾驗,所以被拒絕的變更會讓模型維持逐位元組相同。回歸測試套件斷言的正是這點:在每次被拒絕的呼叫之後,重新讀取表名、縮放、可見性、標準列高、邊距、方向與分頁線計數。載入路徑在低一層得到同樣待遇——在那裡,ZIP 讀取閘為封裝格式協調並行解壓縮
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// 在第一個欄位被碰觸之前取得,絕不在之後
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// 只有完成的最外層防護會推進世代
WriteGuard.Complete;
end;
為什麼巢狀寫入只讓世代推進一次?
因為寫入交易是由執行緒上最外層的防護定義,不是由每個防護各自定義。核心為每個執行緒保存一個寫入者狀態,內含執行緒 id、深度與完成旗標。同一執行緒上第二次 AcquireWriteGuard 會找到那個狀態並遞增 Depth,而不是建立新交易;只有當 Depth 降回零——且最外層防護已被標記 Complete——FGeneration 才會推進。正因如此,Calculate 或 Open 這類高階操作可以在底下呼叫十個受防護的基礎動作,仍登錄為一次變更。內層的 Complete 呼叫會被記錄,但不會自己移動計數器;防護也可以失序釋放,而不破壞帳務
失敗方向同樣明確。如果防護在沒有 Complete 的情況下被釋放——例外回溯解開介面參考的尋常後果——世代不會推進,因為寫入交易從未宣稱成功。請睜眼看清這代表什麼:HotXLS 不會把部分編輯復原。計數器記錄的是「沒有成功交易完成」,這正是快取需要的訊號;但把模型恢復到先前狀態,不是參考計數防護能替您做的事。如果交易中途失敗可能讓活頁簿停在您無法交付的狀態,請保留來源檔案並重新開啟它,而不是相信記憶體中的物件
世代計數器替您買到什麼
不掃描的廉價過時偵測。Generation 是一個從 1 開始、溢位折返時跳過 0 的 UInt64,所以 0 永遠不是核心會核發的值,可充當可靠的「從未觀測」哨兵。兩個不變式讓它可用:任何讀取租用存在期間,世代不能移動;每個成功的寫入交易恰好讓它遞增一次。因此 IXLSWorkbookReadLease.Generation 是在租用整個生命期內保持不變的快照,而 IXLSWorkbookWriteGuard.StartGeneration 告訴寫入者其交易開啟時模型長什麼樣。網格、列印預覽或衍生索引可以比較一個整數,而不必對列做差異比對
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration 從 0 開始,那是核心絕不核發的值,
// 所以最初一趟一定會重建
end;
這套協調不承諾什麼
有三項限制值得直白說明,因為假設相反正是這套機制被誤用的方式。第一,寫入防護不是寫入者之間的互斥:核心把讀取者對寫入者排除,而兩個不同執行緒可以同時各自持有一個寫入防護、各自獨立推進世代——有一個回歸測試斷言的正是這個行為。把您自己的寫入者執行緒序列化,仍是您的工作。第二,這裡沒有任何東西是檔案鎖或跨處理序 mutex;它協調的是單一處理序內、針對單一活頁簿執行個體的各執行緒,兩個開啟同一個 .xlsx 的處理序對彼此一無所知。第三,保證只及於實際取得租用的呼叫端——未租用的讀取仍走在未上鎖的熱門路徑上,那條路徑很快,也完全沒有防護。這是協調核心,不是交易式資料庫
在這些界線內使用,它是個小巧而誠實的基礎型件:九個專用回歸測試涵蓋多讀取者、兩個衝突方向、可重入、失序釋放、中止的交易,以及跨執行緒讀寫與寫寫競爭,全部在一套於 Win32 與 Win64 上通過的 1,328 個測試之中。把它與崩潰安全的分段暫存檔儲存路徑搭配,背景匯出就變成您可以從頭到尾推理的東西——讀取時一致,寫入時不可分割。讀取租用、寫入防護與世代計數器,作為傳統引擎與封裝引擎的一部分,隨適用於 Delphi 與 C++Builder 的 HotXLS Delphi 元件出貨,無需任何設定即可啟用