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ø
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
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
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