Pozadinska dretva izvozila je izvještaj od 40.000 redaka kad je UI dretva postavila jednu ćeliju, a datoteka koja je završila na disku nije odgovarala nijednoj radnoj knjizi koja je ikad postojala. HotXLS tu klasu grešaka rješava u lxWorkbookView.pas, gdje IXLSWorkbookViewCore izdaje O(1) read leaseove i fail-fast write guardove: dok je lease otvoren, svaka ulazna točka mutacije podiže iznimku umjesto da piše
Greška koja stiže bez stack tracea
Čitanje radne knjige nikad nije jedna atomska operacija. Prolaz izvještaja su deseci tisuća pojedinačnih čitanja ćelija rasutih kroz sekunde, a jedan SetValue koji padne između dva njih dovoljan je da promijeni što ostatak prolaza vidi. Klasični motor ovo konkretizira: TXLSCellRef.SetValue može pozvati FSST.Remove da ispusti unos dijeljenog niza, resetira FValueType i poništi stanje predmemorije formule, sve dok druga dretva prolazi kroz dereferenciranje upravo tih struktura. Ništa ne puca na licu mjesta. Dobijete izvještaj čiji se međuzbrojevi ne slažu, ili izvoz koji tiho čita indeks niza koji sada pokazuje negdje drugdje
HotXLS ovo namjerno ne rješava time da pisci čekaju. Čitač može držati radnu knjigu nekoliko sekundi, a u VCL aplikaciji pisac je često UI povratni poziv ili rukovatelj događajem na glavnoj dretvi — blokirati tu dretvu dok pozadinski izvoz ne završi gori je ishod nego što uređivanje padne. Pa koordinacijska jezgra podiže EXLSWorkbookWriteGuardUnavailable u trenutku kad se pokuša pisati uz otvoren lease, prije nego što je dirnuto jedno jedino polje, a pozivatelj odlučuje hoće li uređivanje staviti u red, ponoviti ili reći korisniku. Sukobi fail-fast, ne oni u redu čekanja
Je li radnu knjigu sigurno čitati iz dvije dretve?
Da, pod uvjetom da oba čitača drže lease i da nitko ne piše. IXLSWorkbookViewCore.AcquireReadLease uzima TCriticalSection, povećava brojač, snima trenutnu generaciju i vraća IXLSWorkbookReadLease — konstantno vrijeme bez obzira drži li radna knjiga tisuću ćelija ili milijun. Bilo koji broj leaseova koegzistira, mogu se otpuštati u bilo kojem redoslijedu, a svaki drži jezgru živom kroz vlastitu referencu sučelja, pa lease koji nadživi objekt koji ga je stvorio siguran je, a ne viseći pokazivač. Obadva motora sudjeluju: TXLSWorkbook u lxHandle.pas i TXLSXWorkbook u lxHandleX.pas svaki grade jezgru u svojem konstruktoru i izlažu _AcquireReadLease i _AcquireWriteGuard
Jednako važno jest što lease ne dodaje putanji čitanja. Kritična sekcija pokriva stjecanje leasea, otpuštanje leasea i granice transakcija pisanja — ništa više. Obično čitanje po ćeliji nikad ne ulazi u bravu, monitor ili atomski brojač, pa držanje leasea košta jedno stjecanje i jedno otpuštanje za cijelo skeniranje, ne jedno po ćeliji. To je isti dizajnerski instinkt iza paralelnog parsiranja XLSX-a i rada alokatora memorije: platite koordinaciju na granici, nikad u unutarnjoj petlji. Simetrično pravilo također vrijedi — AcquireReadLease podiže EXLSWorkbookReadLeaseUnavailable kad god je WriteDepth različit od nule, pa ne možete otvoriti lease iz unutar transakcije pisanja, čak ni na dretvi koja piše
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// Podiže EXLSWorkbookReadLeaseUnavailable ako je pisanje u tijeku
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 ovdje napušta doseg: brojač referenci pada na nulu,
// ReleaseReadLease se izvodi i pisci ponovno postaju mogući
end;
Gdje write guard stvarno sjedi?
Na najnižem sloju koji se može mijenjati, nikad na API-ju udobnosti iznad njega. _AcquireWriteGuard zove se iz unutar samog TXLSCellRef.SetValue, što znači da je svaka javna putanja koja se u njega slijeva — Range.Value, dodjela teksta radnog lista, kopiranje ćelija po ćeliju, lijepljenje — gate-irana jednom, umjesto da svaki omotač ponavlja provjeru koju će budući omotač zaboraviti. Pokrivenost je namjerno široka: 55 stjecanja guardova u lxHandle.pas i 37 u lxHandleX.pas stanje u batchu koji je uveo jezgru
Gate-irana površina obuhvaća vrijednosti i formatiranje ćelija, TXLSWorkbook.Open, kopiranje i lijepljenje, definirane nazive (Add, preimenovanje, RefersTo, Visible, IsMacro, Comment, Delete), metapodatke radnog lista poput Name, Zoom, Visible, StandardHeight, FreezePanes, Protect i Activate, postavu stranice, prijelome stranica i Calculate. Smještaj je cijela poanta: guard se stječe prije nego što se napiše prvo polje, a ne validira poslije hookom obavijesti, pa odbijena mutacija ostavlja model bajt-identičnim. Regresijski paket tvrdi upravo to, ponovno čitajući ime lista, zoom, vidljivost, standardnu visinu, margine, orijentaciju i brojeve prijeloma stranica nakon svakog odbijenog poziva. Putanje učitavanja dobivaju isti tretman jedan sloj niže, gdje ZIP read gate koordinira istovremeni inflate za formate paketa
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Stječe se prije nego što se dirne prvo polje, nikad poslije
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Samo dovršeni najvanjskiji guard pomiče generaciju
WriteGuard.Complete;
end;
Zašto ugniježđeno pisanje pomiče generaciju samo jednom?
Jer transakciju pisanja definira najvanjskiji guard na dretvi, a ne svaki guard zasebno. Jezgra drži stanje pisca po dretvi s id-om dretve, dubinom i zastavicom dovršenosti. Drugi AcquireWriteGuard na istoj dretvi nalazi to stanje i povećava Depth umjesto da stvori novu transakciju, a tek kad Depth padne na nulu — uz najvanjskiji guard označen kao Complete — FGeneration napreduje. To omogućuje da operacija visoke razine poput Calculate ili Open zove deset guardiranih primitiva ispod sebe, a registrira se kao jedna promjena. Unutarnji pozivi Complete bilježe se ali sami ne pomiču brojač, a guardovi se mogu otpuštati van reda bez razbijanja računovodstva
Smjer otkaza jednako je izričit. Ako se guard otpusti bez Complete — obična posljedica iznimke koja odmotava referencu sučelja — generacija ne napreduje, jer transakcija pisanja nikad nije tvrdila uspjeh. Jasno gledajte što to znači: HotXLS ne vraća djelomično uređivanje natrag. Brojač bilježi da nijedna uspješna transakcija nije dovršena, što je upravo signal koji predmemorija treba, ali vraćanje modela u prethodno stanje nije nešto što guard brojan referenci može učiniti za vas. Ako neuspjeh usred transakcije može ostaviti radnu knjigu u obliku koji ne možete isporučiti, sačuvajte izvornu datoteku i ponovno je otvorite, umjesto da vjerujete objektu u memoriji
Što vam brojač generacije donosi
Jeftina detekcija zastarjelosti bez skeniranja. Generation je UInt64 koji kreće od 1 i preskače 0 pri omotanju, pa 0 nikad nije vrijednost koju jezgra izdaje i služi kao pouzdan sentinel „nikad viđeno". Dvije invarijante čine ga upotrebljivim: generacija se ne može pomaknuti dok bilo koji read lease postoji, i svaka uspješna transakcija pisanja povećava ga točno jednom. Pa je IXLSWorkbookReadLease.Generation snimak koji ostaje konstantan cijeli život leasea, a IXLSWorkbookWriteGuard.StartGeneration piscu kaže kako je model izgledao kad mu je transakcija otvorena. Mreža, pregled prije ispisa ili izvedeni indeks mogu usporediti jedan cijeli broj umjesto uspoređivanja redaka
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration kreće od 0, vrijednosti koju jezgra nikad ne izdaje,
// pa prvi prolaz uvijek ponovno gradi
end;
Što ova koordinacija ne obećava
Tri ograničenja vrijedi izreći ravnim jezikom, jer je pretpostavljanje suprotnoga način na koji se mehanizam zlouporabi. Prvo, write guard nije međusobno isključivanje pisaca: jezgra isključuje čitače nasuprot piscima, i dvije različite dretve mogu istodobno svaka držati write guard, svaka neovisno pomičući generaciju — regresijski test tvrdi upravo to ponašanje. Serijalizacija vaših vlastitih dretvi pisaca i dalje je vaš posao. Drugo, ništa ovdje nije zaključavanje datoteke ni međuprocesni mutex; koordinira dretve unutar jednog procesa nasuprot jednoj instanci radne knjige, a dva procesa koja otvaraju isti .xlsx jedno o drugome ništa ne znaju. Treće, garancija doseže samo pozivatelje koji stvarno uzmu lease — čitanje bez leasea i dalje ide nebranjeno vrućom putanjom, koja je brza i posve nezaštićena. Ovo je koordinacijska jezgra, a ne transakcijska baza podataka
Korišteno unutar tih granica to je mala, iskrena primitiva: devet namjenskih regresijskih testova pokriva više čitača, oba smjera sukoba, reentranciju, otpuštanje van reda, prekinute transakcije i utrke čitanje/pisanje te pisanje/pisanje između dretvi, unutar paketa od 1.328 testova koji prolaze na Win32 i Win64. Upotrijebite je uz crash-safe putanju spremanja preko privremene datoteke i pozadinski izvoz postaje nešto o čemu možete razmišljati od početka do kraja — dosljedno dok čita, atomski dok piše. Read leaseovi, write guardovi i brojač generacije isporučuju se kao dio klasičnog i paketnog motora u HotXLS Delphi komponenti za Delphi i C++Builder, bez ikakve konfiguracije potrebne da se uključe