Tehnični članak

HotXLS: zaklepi branja in varuhi pisanja v Delphiju

Ozadjska nit je izvažala poročilo s 40.000 vrsticami, ko je nit uporabniškega vmesnika nastavila eno samo celico, datoteka, ki je pristala na disku, pa se ni ujemala z nobenim delovnim zvezkom, ki je kadarkoli obstajal. HotXLS obravnava ta razred napak v lxWorkbookView.pas, kjer IXLSWorkbookViewCore izdaja zaklepe branja O(1) in hitro odpovedoče varuhe pisanja: dokler je zaklep odprt, vsaka vstopna točka spremembe sproži izjemo namesto da bi zapisala

Napaka, ki pride brez skladovne sledi

Branje delovnega zvezka ni nikoli ena sama atomska operacija. Prehod po poročilu je na desetine tisoč posameznih branj celic, raztegnjenih čez sekunde, in en sam SetValue, ki pristane med dvema od njiju, zadošča, da spremeni, kar vidi preostanek prehoda. Klasični pogon to napravi konkretno: TXLSCellRef.SetValue lahko pokliče FSST.Remove, da odstrani vnos iz skupnih nizov, ponastavi FValueType in razveljavi stanje predpomnilnika formul — vse to, medtem ko je druga nit na pol poti skozi dereferenciranje prav teh struktur. Takoj se nič ne zruši. Dobiš poročilo, ki mu vmesne vsote ne gredo skupaj, ali izvoz, ki tiho prebere nizovni indeks, ki zdaj kaže nekam drugam

HotXLS tega namenoma ne rešuje tako, da bi pisalce pustil čakati. Bralec lahko drži delovni zvezek več sekund, v aplikaciji VCL pa je pisalec pogosto povratni klic vmesnika ali ročjevalnik dogodkov na glavni niti — blokirati to nit, dokler se izvoz v ozadju ne zaključi, je slabši izid kot to, da urejanje odpove. Zato jedro usklajevanja sproži EXLSWorkbookWriteGuardUnavailable v trenutku, ko se proti odprtemu zaklepu poskusi pisanje, še preden je bilo dotaknjeno eno samo polje, klicatelj pa se odloči, ali bo spremembo postavil v vrsto, poskusil znova ali povedal uporabniku. Konflikti odpovedo takoj, ne čakajo v vrsti

Matrika usklajevanja HotXLS, ki prikazuje, da zaklepi branja prosto soobstajajo, da pisanje proti odprtemu zaklepu sproži EXLSWorkbookWriteGuardUnavailable, da zaklep, zahtevan znotraj pisalne transakcije, sproži EXLSWorkbookReadLeaseUnavailable, in da se dve pisalni niti nikoli ne izključujeta druga druge
Bralci soobstajajo in pisalci proti njim odpovedo hitro, jedro pa nikoli ne izključi ene pisalne niti zaradi druge

Ali je delovni zvezek varen za branje iz dveh niti?

Da, pod pogojem, da oba bralca držita zaklep in nihče ne piše. IXLSWorkbookViewCore.AcquireReadLease vzame TCriticalSection, poveča števec, ustvari posnetek trenutne generacije in vrne IXLSWorkbookReadLease — v konstantnem času, ne glede na to, ali delovni zvezek vsebuje tisoč celic ali milijon. Poljubno število zaklepov soobstaja, sprostijo se lahko v katerem koli vrstnem redu, vsak pa jedro obdrži pri življenju prek lastnega sklica na vmesnik, zato je zaklep, ki preživi predmet, ki ga je ustvaril, varen in ne viseči kazalec. Sodelujeta oba pogona: TXLSWorkbook v lxHandle.pas in TXLSXWorkbook v lxHandleX.pas vsak v svojem konstruktorju zgradi jedro in izpostavi _AcquireReadLease ter _AcquireWriteGuard

Prav toliko šteje, česa zaklep poti branja ne doda. Kritični odsek pokriva pridobitev zaklepa, sprostitev zaklepa in meje pisalnih transakcij — nič drugega. Običajno branje posamezne celice nikoli ne vstopi v zaklep, monitor ali atomski števec, zato držanje zaklepa stane eno pridobitev in eno sprostitev za celoten pregled, ne eno na celico. To je isti oblikovalski nagon kot pri delu o vzporednem razčlenjevanju XLSX in dodeljevalniku pomnilnika: usklajevanje plačajte na meji, nikoli v notranji zanki. Velja tudi simetrično pravilo — AcquireReadLease sproži EXLSWorkbookReadLeaseUnavailable vsakič, ko WriteDepth ni enak nič, zato zaklepa ne morete odpreti znotraj pisalne transakcije, niti na pisalni niti

HotXLS plača usklajevanje na meji pregleda: kritični odsek pokriva le pridobitev zaklepa, sprostitev in meje pisalnih transakcij, varuh pisanja pa se pridobi znotraj TXLSCellRef.SetValue, zato je vsak priročni API nad njim ograjen enkrat
Ena pridobitev in ena sprostitev pokrijeta pregled petdeset tisoč celic, en sam varuh znotraj TXLSCellRef.SetValue pa pokrije vsako javno pot pisanja nad njim
uses
  lxHandle, lxWorkbookView;

procedure TReportThread.Execute;
var
  Lease: IXLSWorkbookReadLease;
  Sheet: TXLSWorksheet;
  Row: Integer;
  Total: Double;
begin
  // Sproži EXLSWorkbookReadLeaseUnavailable, če je pisanje v teku
  Lease := FWorkbook._AcquireReadLease;
  Sheet := FWorkbook.Sheets[1];
  Total := 0;
  for Row := 1 to 50000 do
    Total := Total + Sheet.Cells[Row, 3].Value;
  FTotal := Total;
  // Zaklep tukaj zapusti obseg: njegov števec sklicev pade na nič,
  // izvede se ReleaseReadLease in pisalci so spet mogoči
end;

Kje pravzaprav sedi varuh pisanja?

V najnižji plasti, ki se lahko spreminja, nikoli v priročnem API-ju nad njo. _AcquireWriteGuard se pokliče znotraj samega TXLSCellRef.SetValue, kar pomeni, da je vsaka javna pot, ki se vanj steka — Range.Value, dodelitev besedila delovnega lista, kopiranje celica eno po eno, lepljenje — ograjena enkrat, namesto da bi vsak ovijalnik ponavljal preverjanje, ki ga bo prihodnji ovijalnik pozabil. Pokritost je namenoma široka: 55 pridobitev varuha v lxHandle.pas in 37 v lxHandleX.pas od serije, ki je uvedla jedro

Ograjena površina obsega vrednosti celic in oblikovanje celic, TXLSWorkbook.Open, kopiranje in lepljenje, definirana imena (Add, preimenovanje, RefersTo, Visible, IsMacro, Comment, Delete), metapodatke delovnega lista, kot so Name, Zoom, Visible, StandardHeight, FreezePanes, Protect in Activate, nastavitve strani, prelome strani in Calculate. Umestitev je celotna poanta: varuh se pridobi, preden se zapiše prvo polje, ne pa, da bi ga pozneje preverila kljuka za obvestila, zato zavrnjena sprememba pusti model bajt za bajtom enak. Preizkusna zbirka regresij trdi prav to: po vsakem zavrnjenem klicu znova prebere ime lista, povečavo, vidnost, standardno višino, robove, usmerjenost in števce prelomov strani. Poti nalaganja dobijo enako obravnavo eno plast nižje, kjer kljuka branja ZIP usklajuje hkratno razpenjanje za formate paketov

procedure TXLSWorksheet.Activate;
var
  WriteGuard: IXLSWorkbookWriteGuard;
begin
  // Pridobljen, preden je dotaknjeno prvo polje, nikoli pozneje
  WriteGuard := FWorkbook._AcquireWriteGuard;
  if not FSelected then
  begin
    FWorkbook.FWorkSheets.Deselect;
    FSelected := True;
  end;
  FWorkbook.FWorkSheets.FActiveSheet := Self;
  // Le dokončan najbolj zunanji varuh dvigne generacijo
  WriteGuard.Complete;
end;

Zakaj gnezdeno pisanje dvigne generacijo le enkrat?

Ker pisalno transakcijo opredeljuje najbolj zunanji varuh na niti, ne pa vsak varuh posebej. Jedro hrani stanje pisalca na vsako nit, ki vsebuje id niti, globino in zastavico dokončanja. Drugi AcquireWriteGuard na isti niti najde to stanje in poveča Depth, namesto da bi ustvaril novo transakcijo, šele ko Depth pade nazaj na nič — pri čemer je bil najbolj zunanji varuh označen kot Complete — pa se dvigne FGeneration. Prav to omogoča, da operacija visoke ravni, kot sta Calculate ali Open, pokliče deset varovanih primitivov pod sabo in se vseeno registrira kot ena sama sprememba. Notranji klici Complete se zabeležijo, števca pa sami po sebi ne premaknejo, varuhe pa je mogoče sprostiti izven vrstnega reda, ne da bi se knjigovodstvo pokvarilo

Smer spodletelosti je enako izrecna. Če se varuh sprosti brez Complete — običajna posledica izjeme, ki odvija sklic na vmesnik — se generacija ne dvigne, ker pisalna transakcija nikoli ni razglasila uspeha. Bodite jasnozrni glede tega, kaj to pomeni: HotXLS delne spremembe ne povrne. Števec zabeleži, da nobena uspešna transakcija ni bila dokončana, kar je točno signal, ki ga potrebuje predpomnilnik, obnovitev modela v prejšnje stanje pa ni tisto, kar bi vam zmogel varuh, voden s števcem sklicev. Če lahko spodletelost sredi transakcije pusti delovni zvezek v obliki, ki je ne morete odposlati, obdržite izvorno datoteko in jo znova odprite, namesto da bi zaupali predmetu v pomnilniku

Primerjani dve časovnici pisalne transakcije HotXLS: gnezdeni varuhi na eni niti dvignejo globino in števec generacij povečajo šele, ko se najbolj zunanji varuh dokonča, medtem ko izjema, ki odvije varuhe brez Complete, pusti generacijo nespremenjeno in delno spremembo na mestu
Depth sledi gnezdenju, a le dokončana najbolj zunanja transakcija dvigne generacijo, prekinjena pa pusti tako števec kot delno spremembo točno tam, kjer sta bila

Kaj vam prinese števec generacij

Poceni zaznavanje zastarelosti brez pregledovanja. Generation je UInt64, ki se začne pri 1 in ob prelivanju preskoči 0, zato 0 nikoli ni vrednost, ki bi jo jedro izdalo, in deluje kot zanesljiv čuvaj »nikoli opaženo«. Dve invarianti naredita števec uporabnim: generacija se ne more premakniti, dokler obstaja kateri koli zaklep branja, vsaka uspešna pisalna transakcija pa jo poveča točno enkrat. Tako je IXLSWorkbookReadLease.Generation posnetek, ki ostane konstanten vse življenje zaklepa, IXLSWorkbookWriteGuard.StartGeneration pa pisalcu pove, kako je izgledal model, ko se je njegova transakcija odprla. Mreža, predogled tiskanja ali izpeljan indeks lahko primerja eno celo število namesto primerjanja vrstic

var
  Lease: IXLSWorkbookReadLease;
begin
  Lease := FWorkbook._AcquireReadLease;
  if Lease.Generation <> FCachedGeneration then
  begin
    FCachedGeneration := Lease.Generation;
    RebuildRowHeightCache;
  end;
  PaintVisibleRows;
  // FCachedGeneration se začne pri 0, vrednosti, ki je jedro nikoli ne izda,
  // zato se prvi prehod vedno znova zgradi
end;

Kaj to usklajevanje ne obljublja

Tri omejitve so vredne izrecne izjave, ker je predpostavka nasprotnega način, kako se mehanizem zlorabi. Prvič, varuh pisanja ni medsebojno izključevanje med pisalci: jedro izključuje bralce pred pisalci, dve različni niti pa lahko hkrati vsaka drži svoj varuh pisanja in neodvisno dviga generacijo — regresijski preizkus trdi točno takšno vedenje. Zaporedjanje lastnih pisalnih niti je še vedno vaša naloga. Drugič, nič od tega ni datotečni zaklep ali medprocesni mutex; usklajuje niti znotraj enega procesa proti eni instanci delovnega zvezka, dva procesa, ki odpirata isto .xlsx, pa drug o drugem ne vesta nič. Tretjič, zagotovilo doseže le klicatelje, ki dejansko vzamejo zaklep — branje brez zaklepa še vedno hodi po neklenčani vroči poti, ki je hitra in povsem nezaščitena. To je jedro usklajevanja, ne transakcijska podatkovna zbirka

Uporabljen znotraj teh meja je majhen, iskren primitiv: devet namenskih regresijskih preizkusov pokriva več bralcev, obe smeri konfliktov, reentranco, sprostitev izven vrstnega reda, prekinjene transakcije ter teke branja/pisanja in pisanja/pisanja čez niti, vse znotraj zbirke 1.328 preizkusov, ki uspevajo na Win32 in Win64. Združite ga s potjo shranjevanja prek stopnjevane začasne datoteke, varno pred sesutjem, in izvoz v ozadju postane nekaj, o čemer lahko razmišljate od začetka do konca — dosledno med branjem, atomsko ob pisanju. Zaklepi branja, varuhi pisanja in števec generacij prihajajo kot del klasičnega in paketnega pogona v HotXLS Delphi Component za Delphi in C++Builder, brez potrebe po kakršni koli nastavitvi za omogočanje