Vlákno na pozadí právě exportovalo přehled o 40 000 řádcích, když vlákno UI nastavilo jednu buňku, a soubor, který dopadl na disk, neodpovídal žádnému sešitu, který kdy existoval. HotXLS řeší tuto třídu chyb v lxWorkbookView.pas, kde IXLSWorkbookViewCore vystavuje read lease s O(1) a write guard s okamžitým selháním: dokud je lease otevřená, každý mutační vstupní bod místo zápisu vyvolá výjimku
Selhání, které přichází bez stack trace
Čtení sešitu nikdy není jediná atomická operace. Průchod přehledem je desetitisíce jednotlivých čtení buněk rozprostřených do sekund a jediný SetValue, který dopadne mezi dvě z nich, stačí ke změně toho, co zbytek průchodu vidí. Klasický engine to ukazuje konkrétně: TXLSCellRef.SetValue může zavolat FSST.Remove a zahodit položku sdíleného řetězce, resetovat FValueType a zneplatnit stav cache vzorců, a to vše, zatímco jiné vlákno je uprostřed dereferencování přesně těchto struktur. Nic nespadne na místě. Dostanete přehled, jehož mezisoučty nesedí, nebo export, který tiše čte index řetězce, který teď ukazuje jinam
HotXLS to neřeší záměrně tím, že by zapisovače nechal čekat. Čtenář může držet sešit několik sekund a ve VCL aplikaci je zapisovač často UI callback nebo obsluha události na hlavním vlákně — zablokovat toto vlákno, dokud export na pozadí neskončí, je horší výsledek než neúspěšná editace. Koordinační jádro proto vyvolá EXLSWorkbookWriteGuardUnavailable v okamžiku, kdy je proti otevřené lease pokus o zápis, dřív, než je dotčeno jediné pole, a volající si rozhodne, zda editaci zařadí do fronty, zopakuje, nebo řekne uživateli. Konflikty selhávají okamžitě, nestojí ve frontě
Je bezpečné číst sešit ze dvou vláken?
Ano, pokud oba čtenáři drží lease a nikdo nezapisuje. IXLSWorkbookViewCore.AcquireReadLease vezme TCriticalSection, zvětší počítadlo, vyfotí aktuální generaci a vrátí IXLSWorkbookReadLease — konstantní čas, ať sešit drží tisíc buněk, nebo milion. Libovolný počet lease koexistuje, mohou být uvolněny v jakémkoli pořadí a každá drží jádro naživu vlastní referencí rozhraní, takže lease, která přežije objekt, který ji vytvořil, je bezpečná, nikoli visící ukazatel. Obě enginy se účastní: TXLSWorkbook v lxHandle.pas a TXLSXWorkbook v lxHandleX.pas si každý postaví jádro ve svém konstruktoru a vystaví _AcquireReadLease a _AcquireWriteGuard
Stejně důležité je, co lease do čtecí cesty nepřidává. Kritická sekce pokrývá získání lease, uvolnění lease a hranice zápisových transakcí — nic jiného. Obyčejné čtení jedné buňky nikdy nevstoupí do zámku, monitoru ani atomického počítadla, takže držení lease stojí jedno získání a jedno uvolnění na celý sken, ne jedno na buňku. To je tentýž návrhový instinkt, který stojí za prací na paralelním parsování XLSX a alokátoru paměti: platit za koordinaci na hranici, nikdy ve vnitřní smyčce. Symetrické pravidlo platí také — AcquireReadLease vyvolá EXLSWorkbookReadLeaseUnavailable, kdykoli je WriteDepth nenulové, takže lease nelze otevřít zevnitř zápisové transakce, ani na zapisujícím vlákně
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// Vyvolá EXLSWorkbookReadLeaseUnavailable, když zápis probíhá
Lease := FWorkbook._AcquireReadLease;
Sheet := FWorkbook.Sheets[1];
Total := 0;
for Row := 1 to 50000 do
Total := Total + Sheet.Cells[Row, 3].Value;
FTotal := Total;
// Lease zde opouští rozsah: její počítadlo referencí klesne na nulu,
// spustí se ReleaseReadLease a zápis se zase stane možným
end;
Kde write guard skutečně sedí?
V nejnižší měnitelné vrstvě, nikdy v pohodlném API nad ní. _AcquireWriteGuard je volán zevnitř samotného TXLSCellRef.SetValue, což znamená, že každá veřejná cesta, která se do něj sleje — Range.Value, přiřazení textu listu, kopírování po buňkách, vložení — je stržena jednou, místo aby každý obal opakoval kontrolu, kterou budoucí obal zapomene. Pokrytí je záměrně široké: 55 získání stráže v lxHandle.pas a 37 v lxHandleX.pas k dávce, která jádro zavedla
Stržený povrch zahrnuje hodnoty a formátování buněk, TXLSWorkbook.Open, kopírování a vkládání, definované názvy (Add, přejmenování, RefersTo, Visible, IsMacro, Comment, Delete), metadata listu jako Name, Zoom, Visible, StandardHeight, FreezePanes, Protect a Activate, nastavení stránky, konce stránek a Calculate. Umístění je celá pointa: stráž se získá dřív, než je napsáno první pole, nikoli se validuje potom háčkem notifikace, takže odmítnutá mutace nechá model bajt identický. Regresní sada to přesně tvrdí: po každém odmítnutém volání znovu načte název listu, zoom, viditelnost, standardní výšku, okraje, orientaci a počty konců stránek. Zavaděčí cesty dostávají totéž o vrstvu níž, kde brána čtení ZIP koordinuje souběžnou inflaci pro formáty balíčků
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Získáno dřív, než je dotčeno první pole, nikdy potom
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Jen dokončená nejvnější stráž posune generaci
WriteGuard.Complete;
end;
Proč vnořený zápis posune generaci jen jednou?
Protože zápisovou transakci definuje nejvnější stráž na vlákně, nikoli každá stráž samostatně. Jádro si drží stav zapisovatele na vlákno obsahující id vlákna, hloubku a příznak dokončení. Druhé AcquireWriteGuard na stejném vlákně ten stav najde a zvětší Depth, místo aby vytvořilo novou transakci, a teprve když Depth spadne zpět na nulu — s nejvnější stráží označenou Complete — posune se FGeneration. To je to, co umožní operaci vysoké úrovně, jako je Calculate nebo Open, zavolat pod ní deset stržených primitiv a stále se zaregistrovat jako jedna změna. Vnitřní volání Complete se zaznamenají, ale samy počítadlo nepohnou, a stráže mohou být uvolněny v nesprávném pořadí, aniž by to rozbilo účtování
Směr selhání je stejně explicitní. Je-li stráž uvolněna bez Complete — obvyklý důsledek výjimky rozvíjející referenci rozhraní — generace neposune, protože zápisová transakce nikdy neuplatnila úspěch. Mějte jasno v tom, co to znamená: HotXLS nevrací částečnou editaci zpět. Počítadlo zaznamená, že žádná úspěšná transakce nedokončila, což je přesně signál, který cache potřebuje, ale obnovení modelu do předchozího stavu není něco, co by za vás udělala stráž počítaná referencemi. Pokud může selhání v polovině transakce zanechat sešit ve tvaru, který nemůžete odeslat, schovejte zdrojový soubor a otevřete jej znovu, místo abyste věřili objektu v paměti
Co vám koupí počítadlo generace
Levná detekce zastarání bez skenování. Generation je UInt64, který začíná na 1 a při přetečení přeskočí 0, takže 0 nikdy není hodnota, kterou jádro vystaví, a slouží jako spolehlivý sentinel „nikdy pozorováno". Dvě invarianty z něj dělají použitelný nástroj: generace se nemůže pohnout, dokud existuje jakákoli read lease, a každá úspěšná zápisová transakce jej zvětší přesně jednou. Takže IXLSWorkbookReadLease.Generation je snímek, který zůstává konstantní po celou dobu života lease, a IXLSWorkbookWriteGuard.StartGeneration řekne zapisovateli, jak model vypadal, když se jeho transakce otevřela. Mřížka, náhled tisku nebo odvozený index mohou porovnat jedno celé číslo, místo aby diffly řádky
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration začíná na 0, hodnotě, kterou jádro nikdy nevydá,
// takže ten úplně první průchod vždy znovu postaví
end;
Co tato koordinace neslibuje
Tři meze stojí za jasné vyjádření, protože předpokládat opak je způsob, jak se mechanizmus zneužívá. Za prvé, write guard není vzájemné vyloučení mezi zapisovateli: jádro vylučuje čtenáře vůči zapisovatelům a dvě různá vlákna mohou současně každé držet write guard a každé nezávisle posouvat generaci — regresní test tvrdí přesně toto chování. Serializovat vlastní vlákna zapisovatelů je stále vaše práce. Za druhé, nic z toho není zámek souboru ani meziprocesová mutex; koordinuje vlákna uvnitř jednoho procesu vůči jedné instanci sešitu a dva procesy otevírající tentýž .xlsx o sobě nic nevědí. Za třetí, záruka dosáhne jen volajících, kteří skutečně lease vezmou — čtení bez lease stále jde odemčenou horkou cestou, která je rychlá a zcela nechráněná. Toto je koordinační jádro, ne transakční databáze
Použito v těchto mezích je malou, poctivou primitivou: devět specializovaných regresních testů pokrývá více čtenářů, oba směry konfliktu, reentranci, uvolnění v nesprávném pořadí, přerušené transakce a závody čtení/zápis a zápis/zápis napříč vlákny, uvnitř sady 1 328 testů procházejících na Win32 a Win64. Spojte jej s cestou ukládání po etapách bezpečnou vůči pádu a export na pozadí se stane něčím, co lze uvažovat od začátku do konce — konzistentní při čtení, atomické při zápisu. Read lease, write guard a počítadlo generace jsou součástí klasického i balíčkového enginu v HotXLS Delphi Component pro Delphi a C++Builder, bez nutnosti jakékoli konfigurace k jejich zapnutí