Tehnički članak

HotXLS read lease-ovi i write guard-ovi za Delphi sveske

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

HotXLS matrica koordinacije koja pokazuje da read lease-ovi slobodno koegzistiraju, da upis pokušan naspram otvorenog lease-a diže EXLSWorkbookWriteGuardUnavailable, da lease zatražen unutar transakcije upisa diže EXLSWorkbookReadLeaseUnavailable, i da dve niti pisca nikad nisu isključene jedna iz druge
Čitači koegzistiraju i pisci brzo padaju naspram njih, ali core nikad ne isključuje jednu nit pisca od druge

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

HotXLS plaća koordinaciju na granici skeniranja: kritična sekcija pokriva samo pribavljanje lease-a, otpuštanje i granice transakcija upisa, dok se write guard pribavlja unutar TXLSCellRef.SetValue pa je svaki convenience API iznad njega kapijaniran jednom
Jedno pribavljanje i jedno otpuštanje pokrivaju skeniranje pedeset hiljada ćelija, i jedan guard unutar TXLSCellRef.SetValue pokriva svaki javni put upisa iznad njega
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 CompleteFGeneration 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

Dve HotXLS vremenske linije transakcije upisa upoređene: ugnježdeni guard-ovi na jednoj niti dižu dubinu i pomeraju brojač generacije tek kada spoljašnji guard završi, dok izuzetak koji odmotava guard-ove bez Complete ostavlja generaciju nepromenjenu i delimičnu izmenu na mestu
Dubina prati ugnježđivanje, ali samo završena spoljašnja transakcija pomera generaciju, i prekinuta ostavlja i brojač i delimičnu izmenu baš tamo gde su bili

Š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