Un fir de fundal exporta un raport de 40.000 de rânduri când firul UI a setat o celulă, iar fișierul ajuns pe disc nu se potrivea cu niciun registru care a existat vreodată. HotXLS tratează clasa asta de bug în lxWorkbookView.pas, unde IXLSWorkbookViewCore emite lease-uri de citire O(1) și garduri de scriere fail-fast: cât timp un lease e deschis, fiecare punct de intrare de mutație ridică excepție în loc să scrie
Eșecul care sosește fără stack trace
Citirea unui registru nu e niciodată o singură operație atomică. O parcurgere de raport e zeci de mii de citiri individuale de celule răspândite peste secunde, și un singur SetValue care aterizează între două dintre ele e de ajuns să schimbe ce vede restul parcurgerii. Motorul clasic face asta concret: TXLSCellRef.SetValue poate apela FSST.Remove pentru a elimina o intrare de shared string, reseta FValueType și invalida o stare de cache de formule, totul în timp ce alt fir e la jumătatea dereferențierii exact a acelor structuri. Nimic nu crapă pe loc. Primiți un raport ale cărui subtotaluri nu se adună, sau un export care citește tăcut un index de șir care acum indică altundeva
HotXLS nu rezolvă deliberat asta făcând scriitorii să aștepte. Un cititor poate ține un registru câteva secunde, iar într-o aplicație VCL scriitorul e adesea un callback UI sau un handler de evenimente pe firul principal — blocarea acelui fir până când un export de fundal se termină e un rezultat mai prost decât eșuarea editării. Deci nucleul de coordonare ridică EXLSWorkbookWriteGuardUnavailable în momentul în care se încearcă o scriere contra unui lease deschis, înainte ca un singur câmp să fi fost atins, iar apelantul decide dacă pune editarea în coadă, reîncearcă sau spune utilizatorului. Conflicte fail-fast, nu puse în coadă
E un registru sigur de citit din două fire?
Da, cu condiția ca ambii cititori să dețină un lease și nimeni să nu scrie. IXLSWorkbookViewCore.AcquireReadLease ia un TCriticalSection, incrementează un contor, fotografiază generația curentă și întoarce un IXLSWorkbookReadLease — timp constant indiferent dacă registrul deține o mie de celule sau un milion. Un număr oarecare de lease-uri coexistă, pot fi eliberate în orice ordine, iar fiecare fixează nucleul în viață prin propria referință de interfață, astfel încât un lease care supraviețuiește obiectului care l-a creat e sigur, nu un pointer atârnat. Ambele motoare participă: TXLSWorkbook în lxHandle.pas și TXLSXWorkbook în lxHandleX.pas construiesc fiecare un nucleu în constructorul lor și expun _AcquireReadLease și _AcquireWriteGuard
Ce contează la fel de mult e ce nu adaugă lease-ul căii de citire. Secțiunea critică acoperă achiziția de lease, eliberarea de lease și granițele de tranzacție de scriere — nimic altceva. Citirea obișnuită per celulă nu intră niciodată într-un lock, un monitor sau un contor atomic, deci deținerea unui lease costă o achiziție și o eliberare pentru toată scanarea, nu una per celulă. E același instinct de design din spatele lucrării de parsare XLSX paralelă și alocator de memorie: plătiți coordonarea la graniță, niciodată în bucla interioară. Regula simetrică ține și ea — AcquireReadLease ridică EXLSWorkbookReadLeaseUnavailable oricând WriteDepth e diferit de zero, deci nu puteți deschide un lease din interiorul unei tranzacții de scriere, nici măcar pe firul care scrie
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// Ridică EXLSWorkbookReadLeaseUnavailable dacă o scriere e în zbor
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-ul iese din scop aici: numărul lui de referințe cade la zero,
// ReleaseReadLease rulează, iar scriitorii devin din nou posibili
end;
Unde stă de fapt gardul de scriere?
La cel mai de jos strat mutabil, niciodată la API-ul de comoditate de deasupra lui. _AcquireWriteGuard e apelat din interiorul lui TXLSCellRef.SetValue însuși, ceea ce înseamnă că fiecare cale publică care se scurge în el — Range.Value, atribuirea de text a foii de lucru, copierea celulă cu celulă, lipirea — e poartată o singură dată în loc ca fiecare wrapper să repete o verificare pe care un wrapper viitor o va uita. Acoperirea e deliberat lată: 55 de achiziții de gard în lxHandle.pas și 37 în lxHandleX.pas la lotul care a introdus nucleul
Suprafața poartată acoperă valorile celulelor și formatarea celulelor, TXLSWorkbook.Open, copierea și lipirea, numele definite (Add, redenumire, RefersTo, Visible, IsMacro, Comment, Delete), metadatele foii de lucru precum Name, Zoom, Visible, StandardHeight, FreezePanes, Protect și Activate, configurarea paginii, întreruperile de pagină și Calculate. Plasamentul e tot punctul: gardul e achiziționat înainte ca primul câmp să fie scris, nu validat după de un hook de notificare, astfel încât o mutație respinsă lasă modelul identic octet cu octet. Suita de regresie afirmă exact asta, recitind numele foii, zoom-ul, vizibilitatea, înălțimea standard, marginile, orientarea și numărurile de întreruperi de pagină după fiecare apel refuzat. Căile de încărcare primesc același tratament cu un strat mai jos, unde poarta de citire ZIP coordonează inflate-ul concurent pentru formatele de pachet
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Achiziționat înainte ca primul câmp să fie atins, niciodată după
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Doar un gard exterior completat avansează generația
WriteGuard.Complete;
end;
De ce o scriere imbricată avansează generația doar o dată?
Pentru că o tranzacție de scriere e definită de gardul cel mai exterior de pe un fir, nu de fiecare gard individual. Nucleul ține o stare de scriitor per fir care deține un id de fir, o adâncime și un flag de completare. Un al doilea AcquireWriteGuard pe același fir găsește acea stare și incrementează Depth în loc să creeze o tranzacție nouă, iar doar când Depth cade înapoi la zero — cu gardul exterior marcat Complete — avansează FGeneration. Asta permite ca o operație de nivel înalt precum Calculate sau Open să apeleze zece primitive poartate dedesubt și să se înregistreze tot ca o singură schimbare. Apelurile interioare Complete sunt înregistrate dar nu mută singure contorul, iar gardurile pot fi eliberate în orice ordine fără a rupe contabilitatea
Direcția de eșec e la fel de explicită. Dacă un gard e eliberat fără Complete — consecința obișnuită a unei excepții care desfășoară referința de interfață — generația nu avansează, pentru că tranzacția de scriere nu a revendicat niciodată succesul. Fiți cu ochii deschiși la ce înseamnă asta: HotXLS nu anulează editarea parțială. Contorul înregistrează că nicio tranzacție reușită nu s-a completat, exact semnalul de care are nevoie un cache, dar restabilirea modelului la starea anterioară nu e ceva ce un gard numărat prin referințe poate face pentru dumneavoastră. Dacă un eșec la mijlocul tranzacției poate lăsa registrul într-o formă pe care nu o puteți livra, păstrați fișierul sursă și redeschideți-l, în loc să aveți încredere în obiectul din memorie
Ce vă cumpără contorul de generație
Detectare ieftină de învechire fără scanare. Generation e un UInt64 care pornește de la 1 și sare peste 0 la învârtire, deci 0 nu e niciodată o valoare emisă de nucleu și funcționează ca o sentinelă fiabilă de „niciodată observat”. Două invariante îl fac utilizabil: generația nu se poate mișca cât timp există vreun lease de citire, iar fiecare tranzacție de scriere reușită îl incrementează exact o dată. Deci IXLSWorkbookReadLease.Generation e o fotografie care rămâne constantă toată viața lease-ului, iar IXLSWorkbookWriteGuard.StartGeneration îi spune unui scriitor cum arăta modelul când tranzacția lui s-a deschis. O grilă, un preview de tipărire sau un index derivat pot compara un întreg în loc să facă diff de rânduri
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration pornește de la 0, o valoare pe care nucleul nu o emite,
// deci chiar prima trecere reconstruiește mereu
end;
Ce nu promite această coordonare
Trei limite merită enunțate pe față, pentru că a presupune altfel e felul în care mecanismul e folosit greșit. În primul rând, un gard de scriere nu e excludere reciprocă între scriitori: nucleul exclude cititorii contra scriitorilor, iar două fire diferite pot deține fiecare un gard de scriere în același timp, fiecare avansând generația independent — un test de regresie afirmă exact acest comportament. Serializarea propriilor fire scriitoare rămâne treaba dumneavoastră. În al doilea rând, nimic aici nu e un lock de fișier sau un mutex cross-proces; coordonează fire în interiorul unui proces contra unei instanțe de registru, iar două procese care deschid același .xlsx nu știu nimic unul despre celălalt. În al treilea rând, garanția ajunge doar la apelanții care chiar iau un lease — o citire fără lease parcurge tot o cale fierbinte neîncuiată, care e rapidă și total neprotejată. Acesta e un nucleu de coordonare, nu o bază de date tranzacțională
Folosit în acele limite e o primitivă mică și onestă: nouă teste de regresie dedicate acoperă cititori multipli, ambele direcții de conflict, reentranța, eliberarea în orice ordine, tranzacțiile abandonate și cursele cross-fir de citire/scriere și scriere/scriere, în interiorul unei suite de 1.328 de teste care trec pe Win32 și Win64. Împerecheați-l cu calea de salvare crash-safe cu fișier temporar etapizat și un export de fundal devine ceva ce puteți raționa cap-coadă — consecvent cât timp citește, atomic când scrie. Lease-urile de citire, gardurile de scriere și contorul de generație sosesc ca parte a motoarelor clasic și pachet în HotXLS Delphi Component pentru Delphi și C++Builder, fără nicio configurație necesară pentru a le activa