Pozadinska nit je izvozila izveštaj od 40.000 redova kada je UI nit postavila jednu ćeliju, i fajl koji je sleteo na disk se ne poklapa ni sa jednom radnom sveskom koja je ikad postojala. HotXLS rukuje tom klasom bagova u lxWorkbookView.pas, gde IXLSWorkbookViewCore izdaje O(1) read lease-ove i fail-fast write guard-ove: dok je lease otvoren, svaka ulazna tačka mutacije diže izuzetak umesto da piše
Greška koja stiže bez stack trace-a
Čitanje radne sveske nikad nije jedna atomska operacija. Prolaz izveštaja je desetine hiljada pojedinačnih čitanja ćelija rasutih kroz sekunde, i jedan SetValue koji slete između dva od njih dovoljan je da promeni šta ostatak prolaza vidi. Klasični engine ovo čini konkretnim: TXLSCellRef.SetValue može pozvati FSST.Remove da otpusti deljeni string unos, resetuje FValueType, i poništi stanje formula keša, sve dok druga nit je usred dereferenciranja baš tih struktura. Ništa ne puca na licu mesta. Dobijete izveštaj čiji se međuzbirovi ne sabiraju, ili izvoz koji tiho čita string indeks koji sada pokazuje negde drugde
HotXLS namerno ne rešava ovo time što tera pisce da čekaju. Čitalac može držati radnu svesku nekoliko sekundi, i u VCL aplikaciji pisac je često UI callback ili event handler na glavnoj niti — blokirati tu nit dok pozadinski izvoz završi je gori ishod od odbijanja izmene. Pa koordinacioni core diže EXLSWorkbookWriteGuardUnavailable u trenutku pokušaja upisa naspram otvorenog lease-a, pre nego što je jedino polje dotaknuto, i pozivalac odlučuje da li da stavi izmenu u red, ponovi, ili kaže korisniku. Konflikti koji padaju brzo, ne oni u redu
Da li je radnu svesku bezbedno čitati iz dve niti?
Da, pod uslovom da oba čitača drže lease i da niko ne piše. IXLSWorkbookViewCore.AcquireReadLease uzima TCriticalSection, povećava brojač, snima trenutnu generaciju, i vraća IXLSWorkbookReadLease — konstantno vreme bez obzira da li radna sveska drži hiljadu ćelija ili milion. Bilo koji broj lease-ova koegzistira, mogu se otpustiti u bilo kom redosledu, i svaki drži core živim kroz sopstvenu referencu interfejsa, pa je lease koji nadživi objekat koji ga je stvorio bezbedan umesto viseci pokazivač. Oba engine-a učestvuju: TXLSWorkbook u lxHandle.pas i TXLSXWorkbook u lxHandleX.pas svaki gradi core u konstruktoru i izlaže _AcquireReadLease i _AcquireWriteGuard
Ono što je podjednako važno je ono što lease ne dodaje putu čitanja. Kritična sekcija pokriva pribavljanje lease-a, otpuštanje lease-a, i granice transakcija upisa — ništa više. Obično čitanje po ćeliji nikad ne ulazi u lok, monitor, ili atomski brojač, pa držanje lease-a košta jedno pribavljanje i jedno otpuštanje za celo skeniranje, ne jedno po ćeliji. To je isti dizajn instinkt iza paralelnog XLSX parsiranja i rada memorijskog alokatora: plati koordinaciju na granici, nikad u unutrašnjoj petlji. Simetrično pravilo takođe važi — AcquireReadLease diže EXLSWorkbookReadLeaseUnavailable kad god je WriteDepth nenult, pa ne možete otvoriti lease iz unutrašnjosti transakcije upisa, čak ni na niti koja piše
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// Digne EXLSWorkbookReadLeaseUnavailable ako upis leti
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 napušta opseg ovde: njegov brojač referenci pada na nulu,
// ReleaseReadLease se izvršava, i pisci ponovo postaju mogući
end;
Gde write guard stvarno sedi?
Na najnižem promenljivom sloju, nikad na convenience API-ju iznad njega. _AcquireWriteGuard se poziva iz unutrašnjosti samog TXLSCellRef.SetValue, što znači da svaki javni put koji se uliva u njega — Range.Value, dodela teksta worksheet-a, kopiranje ćelija po ćeliju, nalepljivanje — je kapijaniran jednom umesto da svaki omotač ponavlja proveru koju će budući omotač zaboraviti. Pokrivenost je namerno široka: 55 pribavljanja guard-a u lxHandle.pas i 37 u lxHandleX.pas stanjem od serije koja je uvela core
Kapijanirana površina obuhvata vrednosti ćelija i formatiranje ćelija, TXLSWorkbook.Open, kopiranje i nalepljivanje, definisana imena (Add, preimenovanje, RefersTo, Visible, IsMacro, Comment, Delete), metapodatke worksheet-a poput Name, Zoom, Visible, StandardHeight, FreezePanes, Protect i Activate, podešavanje stranice, prelome stranice, i Calculate. Plasman je cela poenta: guard se pribavlja pre nego što se prvo polje napiše, ne validira posle kroz notification hook, pa odbijena mutacija ostavlja model bajt-identičnim. Regresiona serija tvrdi baš to, ponovo čitajući ime lista, zoom, vidljivost, standardnu visinu, margine, orijentaciju i brojeve preloma stranice posle svakog odbijenog poziva. Putanje učitavanja dobijaju isti tretman jedan sloj niže, gde ZIP read gate koordinira konkurentan inflate za paketne formate
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Pribavljeno pre nego što se prvo polje dotakne, nikad posle
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Samo završeni spoljašnji guard pomera generaciju
WriteGuard.Complete;
end;
Zašto ugnježdeni upis pomera generaciju samo jednom?
Jer transakciju upisa definiše spoljašnji guard na niti, ne svaki guard pojedinačno. Core čuva stanje pisca po niti koje drži thread id, dubinu i flag završenosti. Drugi AcquireWriteGuard na istoj niti pronalazi to stanje i povećava Depth umesto da stvori novu transakciju, i tek kada Depth padne na nulu — sa spoljašnjim guard-om označenim Complete — FGeneration napreduje. To je ono što visokonivnoj operaciji poput Calculate ili Open dozvoljava da pozove deset kapijaniranih primitiva ispod sebe i dalje se registruje kao jedna promena. Unutrašnji Complete pozivi se evidentiraju ali sami ne pomere brojač, i guard-ovi mogu biti otpušteni van reda ne kvarći evidenciju
Smer greške je jednako eksplicitan. Ako se guard otpusti bez Complete — obična posledica izuzetka koji odmotava referencu interfejsa — generacija ne napreduje, jer transakcija upisa nikad nije tražila uspeh. Gledajte jasno šta to znači: HotXLS ne vraća delimičnu izmenu nazad. Brojač beleži da nijedna uspešna transakcija nije završena, što je baš signal koji keš treba, ali vraćanje modela u prethodno stanje nije nešto što guard sa brojanjem referenci može za vas. Ako greška usred transakcije može da ostavi radnu svesku u obliku koji ne možete isporučiti, sačuvajte izvorni fajl i otvorite ga ponovo, umesto da verate objektu u memoriji
Šta vam brojač generacije kupuje
Jeftina detekcija zastarelosti bez skeniranja. Generation je UInt64 koji počinje od 1 i preskače 0 pri omotavanju, pa 0 nikad nije vrednost koju core izdaje i radi kao pouzdan sentinel „nikad primećen". Dva invarijanta ga čine upotrebljivim: generacija ne može da se pomeri dok bilo koji read lease postoji, i svaka uspešna transakcija upisa povećava ga tačno jednom. Tako je IXLSWorkbookReadLease.Generation snimak koji ostaje konstantan tokom celog života lease-a, a IXLSWorkbookWriteGuard.StartGeneration kaže piscu kako je model izgledao kad mu se transakcija otvorila. Grid, štampački pregled, ili izvedeni indeks mogu porediti jedan ceo broj umesto da razlikuju redove
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration počinje od 0, vrednosti koju core nikad ne izdaje,
// pa prvi prolaz uvek gradi iznova
end;
Šta ova koordinacija ne obećava
Tri ograničenja vredi izreći otvoreno, jer je pretpostavljanje suprotnog način na koji se mehanizam zloupotrebljava. Prvo, write guard nije međusobno isključivanje pisaca: core isključuje čitače naspram pisaca, i dve različite niti mogu istovremeno svaka držati write guard, svaka pomerajući generaciju nezavisno — regresioni test tvrdi baš to ponašanje. Serijalizacija vaših sopstvenih niti pisaca i dalje je vaš posao. Drugo, ništa ovde nije file lock ni cross-process mutex; koordinira niti unutar jednog procesa naspram jedne instance radne sveske, i dva procesa koja otvaraju isti .xlsx ne zna ništa jedno o drugom. Treće, garancija doseže samo pozvaoce koji stvarno uzmu lease — čitanje bez lease-a i dalje hoda otključanom vrelom putanji, koja je brza i potpuno nezaštićena. Ovo je koordinacioni core, ne transakciona baza podataka
Korišćen unutar tih granica, to je mala, iskrena primitiva: devet namenskih regresionih testova pokriva više čitača, oba smera konflikta, reentranciju, otpuštanje van reda, prekinute transakcije, i cross-thread read/write i write/write trke, unutar serije od 1.328 testova koja prolazi na Win32 i Win64. Uporedite ga sa crash-safe staged temp-file putanjom čuvanja i pozadinski izvoz postaje nešto o čemu možete razmišljati od kraja do kraja — dosledan dok čita, atomski kad piše. Read lease-ovi, write guard-ovi i brojač generacije isporučuju se kao deo klasičnog i paketnog engine-a u HotXLS Delphi Komponenti za Delphi i C++Builder, bez ikakve konfiguracije potrebne da se omoguće