Teknisk artikkel

HotXLS leseleier og skrivevoktere for Delphi-arbeidsbøker

En bakgrunnstråd eksporterte en rapport på 40 000 rader da UI-tråden satte én celle, og filen som landet på disken matchet ingen arbeidsbok som noen gang fantes. HotXLS håndterer den klassen av feil i lxWorkbookView.pas, der IXLSWorkbookViewCore utsteder leseleier i O(1) og fail-fast skrivevoktere: mens en leie er åpen, kaster hvert mutasjonsinngangspunkt i stedet for å skrive

Feilen som ankommer uten stack trace

Å lese en arbeidsbok er aldri én atomær operasjon. Et rapportgjennomløp er titusenvis av enkeltcelle-lesinger spredt over sekunder, og én enkelt SetValue som lander mellom to av dem er nok til å endre hva resten av gjennomløpet ser. Den klassiske motoren gjør dette konkret: TXLSCellRef.SetValue kan kalle FSST.Remove for å droppe en delt strengoppføring, nullstille FValueType og ugyldiggjøre en formelbufertilstand, alt mens en annen tråd er midtveis i å dereferere nøyaktig de strukturene. Ingenting krasjer der og da. Du får en rapport der delsummer ikke går opp, eller en eksport som stille leser en strengindeks som nå peker et annet sted

HotXLS løser ikke dette ved å la skribenter vente, med vilje. En leser kan holde en arbeidsbok i flere sekunder, og i en VCL-applikasjon er skribenten ofte et UI-callback eller en hendelsesbehandler på hovedtråden — å blokkere den tråden til en bakgrunnseksport fullfører er et verre utfall enn å feile redigeringen. Så koordineringskjernen kaster EXLSWorkbookWriteGuardUnavailable i det øyeblikket et skriveforsøk gjøres mot en åpen leie, før ett enkelt felt er berørt, og kalleren bestemmer om redigeringen skal settes i kø, prøves på nytt, eller fortelles brukeren. Konflikter feiler raskt, de settes ikke i kø

En HotXLS-koordinasjonsmatrise som viser at leseleier eksisterer fritt side om side, at et skriveforsøk mot en åpen leie kaster EXLSWorkbookWriteGuardUnavailable, at en leie forespurt inne i en skrivetransaksjon kaster EXLSWorkbookReadLeaseUnavailable, og at to skribenttråder aldri ekskluderes fra hverandre
Lesere eksisterer side om side og skribenter feiler raskt mot dem, men kjernen ekskluderer aldri én skribenttråd fra en annen

Er en arbeidsbok trygg å lese fra to tråder?

Ja, forutsatt at begge lesere holder en leie og ingen skriver. IXLSWorkbookViewCore.AcquireReadLease tar en TCriticalSection, inkrementerer en teller, tar et øyeblikksbilde av den gjeldende generasjonen, og returnerer en IXLSWorkbookReadLease — konstant tid uavhengig av om arbeidsboken holder tusen celler eller en million. Et vilkårlig antall leier eksisterer side om side, de kan frigis i vilkårlig rekkefølge, og hver enkelt holder kjernen i live gjennom sin egen grensesnittreferanse, så en leie som overlever objektet som skapte den, er trygg snarere enn en dinglende peker. Begge motorer deltar: TXLSWorkbook i lxHandle.pas og TXLSXWorkbook i lxHandleX.pas bygger hver sin kjerne i konstruktøren og eksponerer _AcquireReadLease og _AcquireWriteGuard

Det som betyr like mye, er hva leien ikke legger til lesestien. Den kritiske seksjonen dekker å ta en leie, frigjøre en leie og skrivetransaksjonsgrenser — ingenting annet. Den ordinære per-celle-lesingen går aldri inn i en lås, en monitor eller en atomær teller, så å holde en leie koster én innhenting og én frigivelse for hele skanningen, ikke én per celle. Det er samme designinstinkt bak parallelt XLSX-parsing- og minneallokator-arbeid: betal for koordinering ved grensen, aldri i den indre løkken. Den symmetriske regelen gjelder også — AcquireReadLease kaster EXLSWorkbookReadLeaseUnavailable når WriteDepth ikke er null, så du kan ikke åpne en leie fra innsiden av en skrivetransaksjon, ikke engang på skrivetråden

HotXLS betaler for koordinering ved grensen av en skanning: den kritiske seksjonen dekker bare leie-innhenting, frigivelse og skrivetransaksjonsgrenser, mens skrivevokteren hentes inn inne i TXLSCellRef.SetValue slik at hvert bekvemmelighets-API over den porteres én gang
Én innhenting og én frigivelse dekker en skanning på femti tusen celler, og én enkelt vokter inne i TXLSCellRef.SetValue dekker hver offentlig skrivesti over den
uses
  lxHandle, lxWorkbookView;

procedure TReportThread.Execute;
var
  Lease: IXLSWorkbookReadLease;
  Sheet: TXLSWorksheet;
  Row: Integer;
  Total: Double;
begin
  // Kaster EXLSWorkbookReadLeaseUnavailable hvis et skriv er underveis
  Lease := FWorkbook._AcquireReadLease;
  Sheet := FWorkbook.Sheets[1];
  Total := 0;
  for Row := 1 to 50000 do
    Total := Total + Sheet.Cells[Row, 3].Value;
  FTotal := Total;
  // Leien forlater omfanget her: referansetallet faller til null,
  // ReleaseReadLease kjører, og skribenter blir mulig igjen
end;

Hvor sitter skrivevokteren egentlig?

På det laveste mutbare laget, aldri på bekvemmelighets-API-et ovenfor. _AcquireWriteGuard kalles fra innsiden av TXLSCellRef.SetValue selv, som betyr at hver offentlig sti som trakterer inn i den — Range.Value, teksttilordning for regneark, celle-for-celle-kopiering, innliming — porteres én gang i stedet for at hver wrapper gjentar en sjekk en fremtidig wrapper vil glemme. Dekningen er bevisst bred: 55 vokter-innhentinger i lxHandle.pas og 37 i lxHandleX.pas per bunten som introduserte kjernen

Den porterte overflaten spenner over celleverdier og celleformatering, TXLSWorkbook.Open, kopiering og innliming, definerte navn (Add, omdøping, RefersTo, Visible, IsMacro, Comment, Delete), regneark-metadata som Name, Zoom, Visible, StandardHeight, FreezePanes, Protect og Activate, sideoppsett, sideskift og Calculate. Plasseringen er hele poenget: vokteren hentes inn før det første feltet skrives, ikke valideres etterpå av en varslingskrok, så en avvist mutasjon etterlater modellen byte-identisk. Regresjonspakken hevder nøyaktig det, ved å lese arknavn, zoom, synlighet, standard høyde, marginer, orientering og antall sideskift på nytt etter hvert avslått kall. Lastestier får samme behandling ett lag ned, der ZIP-leseporten koordinerer samtidig inflate for pakkeformater

procedure TXLSWorksheet.Activate;
var
  WriteGuard: IXLSWorkbookWriteGuard;
begin
  // Hentet inn før det første feltet berøres, aldri etter
  WriteGuard := FWorkbook._AcquireWriteGuard;
  if not FSelected then
  begin
    FWorkbook.FWorkSheets.Deselect;
    FSelected := True;
  end;
  FWorkbook.FWorkSheets.FActiveSheet := Self;
  // Bare en fullført ytterste vokter flytter generasjonen
  WriteGuard.Complete;
end;

Hvorfor flytter et nestet skriv generasjonen bare én gang?

Fordi en skrivetransaksjon defineres av den ytterste vokteren på en tråd, ikke av hver vokter hver for seg. Kjernen holder en per-tråd skribenttilstand med en tråd-id, en dybde og et fullføringsflagg. En andre AcquireWriteGuard på samme tråd finner den tilstanden og inkrementerer Depth i stedet for å lage en ny transaksjon, og først når Depth faller tilbake til null — med den ytterste vokteren markert Complete — flytter FGeneration seg. Dette er det som lar en operasjon på høyt nivå som Calculate eller Open kalle ti voktede primitiver under seg og fortsatt registreres som én endring. Indre Complete-kall registreres, men flytter ikke telleren på egen hånd, og vokterne kan frigis i feil rekkefølge uten å bryte regnskapet

Feilretningen er like eksplisitt. Hvis en vokter frigis uten Complete — den ordinære konsekvensen av et unntak som pakker ut grensesnittreferansen — flytter generasjonen seg ikke, fordi skrivetransaksjonen aldri hevdet suksess. Vær klarøyd på hva det betyr: HotXLS ruller ikke den delvise redigeringen tilbake. Telleren registrerer at ingen vellykket transaksjon fullførte, som er nøyaktig signalet en hurtigbuffer trenger, men å gjenopprette modellen til sin forrige tilstand er ikke noe en referansetalt vokter kan gjøre for deg. Hvis en feil midt i en transaksjon kan etterlate arbeidsboken i en form du ikke kan sende, behold kildefilen og åpne den på nytt, i stedet for å stole på objektet i minnet

To HotXLS skrivetransaksjonstidslinjer sammenlignet: nestede voktere på én tråd hever dybden og flytter generasjonstelleren bare når den ytterste vokteren fullfører, mens et unntak som pakker ut vokterne uten Complete lar generasjonen stå uendret og den delvise redigeringen på plass
Depth sporer nestingen, men bare en fullført ytterste transaksjon flytter generasjonen, og en avbrutt lar både telleren og den delvise redigeringen stå nøyaktig der de var

Hva generasjonstelleren gir deg

Billig foreldet-deteksjon uten skanning. Generation er en UInt64 som starter på 1 og hopper over 0 ved overløp, så 0 er aldri en verdi kjernen utsteder og fungerer som en pålitelig «aldri observert»-sentinel. To invarianter gjør den brukbar: generasjonen kan ikke flytte seg mens noen leseleie finnes, og hver vellykket skrivetransaksjon inkrementerer den nøyaktig én gang. Så IXLSWorkbookReadLease.Generation er et øyeblikksbilde som forblir konstant gjennom hele leiens levetid, og IXLSWorkbookWriteGuard.StartGeneration forteller en skribent hvordan modellen så ut da transaksjonen åpnet. Et rutenett, en forhåndsvisning av utskrift eller en avledet indeks kan sammenligne ett heltall i stedet for å differe rader

var
  Lease: IXLSWorkbookReadLease;
begin
  Lease := FWorkbook._AcquireReadLease;
  if Lease.Generation <> FCachedGeneration then
  begin
    FCachedGeneration := Lease.Generation;
    RebuildRowHeightCache;
  end;
  PaintVisibleRows;
  // FCachedGeneration starter på 0, en verdi kjernen aldri utsteder,
  // så det aller første passet bygger alltid på nytt
end;

Hva denne koordineringen ikke lover

Tre grenser er verdt å si klart ut, fordi å anta noe annet er hvordan mekanismen blir misbrukt. For det første er ikke en skrivevokter gjensidig utelukkelse mellom skribenter: kjernen ekskluderer lesere mot skribenter, og to forskjellige tråder kan hver holde en skrivevokter samtidig, hver som flytter generasjonen uavhengig — en regresjonstest hevder nøyaktig denne oppførselen. Å serialisere dine egne skribenttråder er fortsatt din jobb. For det andre er ingenting her en fillås eller en kryssprosess-mutex; den koordinerer tråder inne i én prosess mot én arbeidsbokforekomst, og to prosesser som åpner samme .xlsx vet ingenting om hverandre. For det tredje når garantien bare kallere som faktisk tar en leie — en lesing uten leie går fortsatt en ulåst varm sti, som er rask og fullstendig ubeskyttet. Dette er en koordineringskjerne, ikke en transaksjonell database

Brukt innenfor disse grensene er det en liten, ærlig primitiv: ni dedikerte regresjonstester dekker flere lesere, begge konfliktretningene, reentrans, frigivelse i feil rekkefølge, avbrutte transaksjoner og kryss-tråd lesing/skriving og skriving/skriving-race, inne i en pakke på 1 328 tester som består på Win32 og Win64. Kombiner den med den krasjsikre trinnvise temp-fillagreringsstien og en bakgrunnseksport blir noe du kan resonnere om ende til ende — konsistent mens den leser, atomær når den skriver. Leseleier, skrivevoktere og generasjonstelleren kommer som del av de klassiske og pakke-motorene i HotXLS Delphi Component for Delphi og C++Builder, uten konfigurering nødvendig for å aktivere dem