Teknisk artikel

HotXLS-læseleases og skrivevagter til Delphi-arbejdsbøger

En baggrundstråd var ved at eksportere en rapport på 40.000 rækker, mens UI-tråden satte én celle, og den fil, der landede på disken, svarede til ingen arbejdsbog, der nogensinde havde eksisteret. HotXLS håndterer den klasse af fejl i lxWorkbookView.pas, hvor IXLSWorkbookViewCore udsteder O(1)-læseleases og fail-fast-skrivevagter: mens en lease er åben, rejser hvert mutationsindgangspunkt i stedet for at skrive

Fejlen, der ankommer uden et stakspor

At læse en arbejdsbog er aldrig én atomar operation. Et rapportgennemløb er titusindvis af enkelte cellelæsninger fordelt over sekunder, og en enkelt SetValue, der lander mellem to af dem, er nok til at ændre, hvad resten af gennemløbet ser. Den klassiske motor gør det konkret: TXLSCellRef.SetValue kan kalde FSST.Remove for at droppe en delt strengpost, nulstille FValueType og invalidere en formelcache-tilstand, alt imens en anden tråd er midt i at dereferere netop de strukturer. Intet crasher på stedet. Du får en rapport, hvis delsummer ikke længere går op, eller en eksport, der stille læser et strengindeks, der nu peger et andet sted

HotXLS løser ikke dette ved at få skrivere til at vente. En læser kan holde en arbejdsbog i flere sekunder, og i en VCL-applikation er skriveren ofte et UI-callback eller en eventhandler på hovedtråden — at blokere den tråd, til en baggrundseksport er færdig, er et værre udfald end at fejle redigeringen. Koordinationskernen rejser derfor EXLSWorkbookWriteGuardUnavailable i det øjeblik, et skriveforsøg rettes mod en åben lease, før et enkelt felt er rørt, og kalderen beslutter, om redigeringen skal sættes i kø, prøves igen eller meldes til brugeren. Fail-fast-konflikter, ikke køede

En HotXLS-koordinationsmatrix, der viser, at læseleases frit kan eksistere side om side, at et skriveforsøg mod en åben lease rejser EXLSWorkbookWriteGuardUnavailable, at en lease, der anmodes inde i en skrivetransaktion, rejser EXLSWorkbookReadLeaseUnavailable, og at to skrivetråde aldrig udelukker hinanden
Læsere eksisterer side om side, og skrivere fejler hurtigt over for dem, men kernen udelukker aldrig én skrivetråd fra en anden

Er en arbejdsbog sikker at læse fra to tråde?

Ja, forudsat at begge læsere holder en lease, og ingen skriver. IXLSWorkbookViewCore.AcquireReadLease tager en TCriticalSection, øger en tæller, tager et snapshot af den aktuelle generation og returnerer en IXLSWorkbookReadLease — konstant tid, uanset om arbejdsbogen holder tusind celler eller en million. Et vilkårligt antal leases kan eksistere side om side, de kan frigives i vilkårlig rækkefølge, og hver enkelt holder kernen i live gennem sin egen interfacereference, så en lease, der overlever det objekt, der oprettede den, er sikker frem for at være en danglende pointer. Begge motorer deltager: TXLSWorkbook i lxHandle.pas og TXLSXWorkbook i lxHandleX.pas bygger hver sin kerne i deres konstruktør og eksponerer _AcquireReadLease og _AcquireWriteGuard

Lige så vigtigt er det, hvad leasen ikke tilføjer til læsestien. Det kritiske afsnit dækker leaseanskaffelse, leasefrigivelse og skrivetransaktionsgrænser — intet andet. Den almindelige pr. celle-læsning træder aldrig ind i en lock, en monitor eller en atomar tæller, så det at holde en lease koster én anskaffelse og én frigivelse for hele gennemløbet, ikke én pr. celle. Det er samme designinstinkt som bag arbejdet med parallel XLSX-parsing og memory allocator: betal for koordinering ved grænsen, aldrig i den indre løkke. Den symmetriske regel gælder også — AcquireReadLease rejser EXLSWorkbookReadLeaseUnavailable, hver gang WriteDepth er forskellig fra nul, så du kan ikke åbne en lease indefra en skrivetransaktion, heller ikke på skrivetråden

HotXLS betaler for koordinering ved grænsen af et gennemløb: det kritiske afsnit dækker kun leaseanskaffelse, frigivelse og skrivetransaktionsgrænser, mens skrivevagten anskaffes inde i TXLSCellRef.SetValue, så hvert bekvemmeligheds-API ovenover bliver gatekeepet én gang
Én anskaffelse og én frigivelse dækker et gennemløb af halvtreds tusind celler, og en enkelt vagt inde i TXLSCellRef.SetValue dækker enhver offentlig skrivesti ovenover
uses
  lxHandle, lxWorkbookView;

procedure TReportThread.Execute;
var
  Lease: IXLSWorkbookReadLease;
  Sheet: TXLSWorksheet;
  Row: Integer;
  Total: Double;
begin
  // Rejser EXLSWorkbookReadLeaseUnavailable, hvis en skrivning er i gang
  Lease := FWorkbook._AcquireReadLease;
  Sheet := FWorkbook.Sheets[1];
  Total := 0;
  for Row := 1 to 50000 do
    Total := Total + Sheet.Cells[Row, 3].Value;
  FTotal := Total;
  // Leasen forlader scope her: dens referencetal falder til nul,
  // ReleaseReadLease kører, og skrivere bliver mulige igen
end;

Hvor sidder skrivevagten reelt?

På det laveste mutbare lag, aldrig ved det bekvemmeligheds-API, der ligger ovenover. _AcquireWriteGuard kaldes indefra TXLSCellRef.SetValue selv, hvilket betyder, at enhver offentlig sti, der løber ud i den — Range.Value, tildeling af regnearkstekst, kopiering celle for celle, indsætning — bliver gatekeepet én gang, i stedet for at hver wrapper gentager en kontrol, som en fremtidig wrapper glemmer. Dækningen er bevidst bred: 55 vagtanskaffelser i lxHandle.pas og 37 i lxHandleX.pas pr. den batch, der indførte kernen

Den gatekeepede flade spænder over celleværdier og celleformatering, TXLSWorkbook.Open, kopiering og indsætning, definerede navne (Add, omdøbning, RefersTo, Visible, IsMacro, Comment, Delete), regnearksmetadata som Name, Zoom, Visible, StandardHeight, FreezePanes, Protect og Activate, sideopsætning, sideskift og Calculate. Placeringen er hele pointen: vagten anskaffes, før det første felt skrives, ikke valideret bagefter af en notifikationshook, så en afvist mutation efterlader modellen byte-identisk. Regressionstestpakken hævder præcis det ved at genlæse arknavn, zoom, synlighed, standardhøjde, margener, orientering og antal sideskift efter hvert afvist kald. Indlæsningsstier får samme behandling ét lag nede, hvor ZIP-læseporten koordinerer samtidig inflate for pakkeformater

procedure TXLSWorksheet.Activate;
var
  WriteGuard: IXLSWorkbookWriteGuard;
begin
  // Anskaffet, før det første felt røres, aldrig bagefter
  WriteGuard := FWorkbook._AcquireWriteGuard;
  if not FSelected then
  begin
    FWorkbook.FWorkSheets.Deselect;
    FSelected := True;
  end;
  FWorkbook.FWorkSheets.FActiveSheet := Self;
  // Kun en fuldført yderste vagt fremrykker generationen
  WriteGuard.Complete;
end;

Hvorfor fremrykker en indlejret skrivning generationen kun én gang?

Fordi en skrivetransaktion defineres af den yderste vagt på en tråd, ikke af hver vagt enkeltvis. Kernen holder en pr. tråd-skrivertilstand med et tråd-id, en dybde og et fuldførelsesflag. En anden AcquireWriteGuard på samme tråd finder den tilstand og øger Depth i stedet for at oprette en ny transaktion, og først når Depth falder tilbage til nul — med den yderste vagt markeret Complete — rykker FGeneration. Det er det, der gør, at en operation på højt niveau som Calculate eller Open kan kalde ti vagtede primitiver nedenunder og stadig registreres som én ændring. Indre Complete-kald registreres, men flytter ikke tælleren på egen hånd, og vagterne kan frigives i forkert rækkefølge uden at ødelægge regnskabet

Fejlretningen er lige så eksplicit. Hvis en vagt frigives uden Complete — den almindelige konsekvens af en exception, der vikler interfacereferencen op — rykker generationen ikke, fordi skrivetransaktionen aldrig krævede succes. Vær klar over, hvad det betyder: HotXLS ruller ikke den delvise redigering tilbage. Tælleren registrerer, at ingen vellykket transaktion blev fuldført, hvilket er præcis det signal, en cache har brug for, men at genoprette modellen til dens tidligere tilstand er ikke noget, en referencetalt vagt kan gøre for dig. Hvis en fejl midt i transaktionen kan efterlade arbejdsbogen i en form, du ikke kan afskibe, så behold kildefilen og genåbn den frem for at stole på objektet i hukommelsen

To HotXLS-skrivetransaktionstidslinjer sammenlignet: indlejrede vagter på én tråd øger dybden og fremrykker generationstælleren kun, når den yderste vagt fuldføres, mens en exception, der vikler vagterne op uden Complete, efterlader generationen uændret og den delvise redigering på plads
Dybden sporer indlejringen, men kun en fuldført yderste transaktion fremrykker generationen, og en afbrudt efterlader både tælleren og den delvise redigering præcis, hvor de var

Hvad generationstælleren giver dig

Billig forældelsesdetektion uden scanning. Generation er en UInt64, der starter på 1 og springer 0 over ved wraparound, så 0 aldrig er en værdi, kernen udsteder, og fungerer som en pålidelig »aldrig observeret«-sentinel. To invarianter gør den brugbar: generationen kan ikke rykke, mens der findes en læselease, og hver vellykket skrivetransaktion øger den præcis én gang. Så IXLSWorkbookReadLease.Generation er et snapshot, der forbliver konstant i hele leasens levetid, og IXLSWorkbookWriteGuard.StartGeneration fortæller en skriver, hvordan modellen så ud, da dens transaktion åbnede. Et grid, en udskriftsforhåndsvisning eller et afledt indeks kan sammenligne ét heltal i stedet for at diffe rækker

var
  Lease: IXLSWorkbookReadLease;
begin
  Lease := FWorkbook._AcquireReadLease;
  if Lease.Generation <> FCachedGeneration then
  begin
    FCachedGeneration := Lease.Generation;
    RebuildRowHeightCache;
  end;
  PaintVisibleRows;
  // FCachedGeneration starter på 0, en værdi kernen aldrig udsteder,
  // så det allerførste gennemløb genopbygger altid
end;

Hvad denne koordinering ikke lover

Tre grænser er værd at slå fast rent ud, for antagelser om det modsatte er måden, mekanismen misbruges på. For det første er en skrivevagt ikke gensidig udelukkelse mellem skrivere: kernen udelukker læsere mod skrivere, og to forskellige tråde kan hver holde en skrivevagt på samme tid og hver fremrykke generationen uafhængigt — en regressionstest hævder præcis denne adfærd. At serialisere dine egne skrivetråde er stadig dit arbejde. For det andet er intet her en fillås eller en cross-process-mutex; det koordinerer tråde inde i én proces mod én arbejdsbogsinstans, og to processer, der åbner samme .xlsx, ved intet om hinanden. For det tredje når garantien kun kalder, der faktisk tager en lease — en læsning uden lease går stadig ad en ulåst hot path, som er hurtig og helt ubeskyttet. Dette er en koordinationskerne, ikke en transaktionel database

Brugt inden for disse grænser er det en lille, ærlig primitiv: ni dedikerede regressionstest dækker flere læsere, begge konfliktretninger, reentrans, frigivelse i forkert rækkefølge, afbrudte transaktioner og cross-thread-læse/skrive- og skrive/skrive-races, inde i en pakke med 1.328 test, der består på Win32 og Win64. Kombinér den med den crash-sikre trinvise gem-til-midlertidig-fil-sti, og en baggrundseksport bliver noget, du kan ræsonnere om ende til ende — konsistent, mens den læser, atomar, når den skriver. Læseleases, skrivevagter og generationstælleren følger med som en del af de klassiske og pakkebaserede motorer i HotXLS Delphi Component til Delphi og C++Builder, uden at der kræves konfiguration for at aktivere dem