Technisch artikel

HotXLS read leases en write guards voor Delphi-werkmappen

Een achtergrondthread exporteerde een rapport van 40.000 rijen toen de UI-thread één cel zette, en het bestand dat op schijf belandde leek op geen enkele werkmap die ooit heeft bestaan. HotXLS pakt die klasse van bugs aan in lxWorkbookView.pas, waar IXLSWorkbookViewCore O(1) read leases en fail-fast write guards uitgeeft: terwijl een lease open is, gooit elk mutatie-instappunt een exception in plaats van te schrijven

De fout die aankomt zonder stack trace

Een werkmap lezen is nooit één atomaire operatie. Een rapportwandeling is tienduizenden losse celezen verspreid over seconden, en één enkele SetValue die tussen twee daarvan landt is genoeg om te veranderen wat de rest van de wandeling ziet. De klassieke engine maakt dit concreet: TXLSCellRef.SetValue kan FSST.Remove aanroepen om een shared-string-entry te laten vallen, FValueType resetten en een formulecachestatus ongeldig maken, allemaal terwijl een andere thread halverwege precies die structuren dereferenceert. Niets crasht ter plekke. U krijgt een rapport waarvan de subtotalen niet opgaan, of een export die geruisloos een string-index leest die nu ergens anders naartoe wijst

HotXLS lost dit bewust niet op door schrijvers te laten wachten. Een lezer kan een werkmap enkele seconden vasthouden, en in een VCL-applicatie is de schrijver vaak een UI-callback of een eventhandler op de hoofdthread — die thread blokkeren tot een achtergrondexport klaar is is een slechtere uitkomst dan de bewerking laten mislukken. De coördinatiekern gooit daarom EXLSWorkbookWriteGuardUnavailable op het moment dat een schrijfactie wordt geprobeerd tegen een open lease, voordat één veld is aangeraakt, en de aanroeper beslist of hij de bewerking in de wachtrij zet, het opnieuw probeert, of het aan de gebruiker vertelt. Fail-fast conflicten, geen wachtrijconflicten

Een HotXLS-coördinatiematrix die toont dat read leases vrij samenkomen, dat een schrijfactie tegen een open lease EXLSWorkbookWriteGuardUnavailable gooit, dat een lease binnen een schrijftransactie EXLSWorkbookReadLeaseUnavailable gooit, en dat twee schrijverthreads elkaar nooit uitsluiten
Lezers komen samen en schrijvers falen snel tegen hen, maar de kern sluit nooit één schrijverthread uit van een andere

Is een werkmap veilig te lezen vanuit twee threads?

Ja, mits beide lezers een lease vasthouden en niemand schrijft. IXLSWorkbookViewCore.AcquireReadLease gebruikt een TCriticalSection, verhoogt een teller, neemt een momentopname van de huidige generatie en geeft een IXLSWorkbookReadLease terug — constante tijd, ongeacht of de werkmap duizend cellen of een miljoen bevat. Elk aantal leases kan samen bestaan, ze mogen in willekeurige volgorde worden vrijgegeven, en elk houdt de kern levend via zijn eigen interfacereferentie, dus een lease die het creërende object overleeft is veilig in plaats van een dangling pointer. Beide engines doen mee: TXLSWorkbook in lxHandle.pas en TXLSXWorkbook in lxHandleX.pas bouwen elk een kern in hun constructor en stellen _AcquireReadLease en _AcquireWriteGuard beschikbaar

Even belangrijk is wat de lease niet toevoegt aan het leespad. De critical section dekt lease-acquisitie, lease-vrijgave en schrijftransactiegrenzen — niets anders. De gewone lees-per-cel gaat nooit een lock, een monitor of een atome teller in, dus een lease vasthouden kost één acquisitie en één vrijgave voor de hele scan, niet één per cel. Dat is dezelfde ontwerpinstant achter het parallele XLSX-parsen en geheugenallocator-werk: betaal voor coördinatie aan de grens, nooit in de binnenlus. De symmetrische regel geldt ook — AcquireReadLease gooit EXLSWorkbookReadLeaseUnavailable zodra WriteDepth niet nul is, dus u kunt geen lease openen vanuit een schrijftransactie, zelfs niet op de schrijfthread

HotXLS betaalt voor coördinatie aan de grens van een scan: de critical section dekt alleen lease-acquisitie, vrijgave en schrijftransactiegrenzen, terwijl de write guard binnen TXLSCellRef.SetValue wordt verkregen zodat elke handige API erboven één keer wordt begrend
Eén acquisitie en één vrijgave dekken een scan van vijftigduizend cellen, en één enkele guard binnen TXLSCellRef.SetValue dekt elk publiek schrijfpad erboven
uses
  lxHandle, lxWorkbookView;

procedure TReportThread.Execute;
var
  Lease: IXLSWorkbookReadLease;
  Sheet: TXLSWorksheet;
  Row: Integer;
  Total: Double;
begin
  // Gooit EXLSWorkbookReadLeaseUnavailable als een schrijfactie in vlucht is
  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 verlaat hier het bereik: zijn referentietelling valt naar nul,
  // ReleaseReadLease draait, en schrijvers worden weer mogelijk
end;

Waar zit de write guard feitelijk?

Op de laagste muteerbare laag, nooit bij de handige API erboven. _AcquireWriteGuard wordt aangeroepen vanuit TXLSCellRef.SetValue zelf, wat betekent dat elk publiek pad dat erin uitmondt — Range.Value, werkbladteksttoewijzing, cel-voor-cel kopiëren, plakken — één keer wordt begrend in plaats van dat elke wrapper een controle herhaalt die een toekomstige wrapper zal vergeten. De dekking is bewust breed: 55 guard-acquisities in lxHandle.pas en 37 in lxHandleX.pas ten tijde van de batch die de kern introduceerde

Het begrendde oppervlak omvat celwaarden en celopmaak, TXLSWorkbook.Open, kopiëren en plakken, gedefinieerde namen (Add, hernoemen, RefersTo, Visible, IsMacro, Comment, Delete), werkbladmetadata zoals Name, Zoom, Visible, StandardHeight, FreezePanes, Protect en Activate, pagina-instelling, pagina-einden en Calculate. Plaatsing is het hele punt: de guard wordt verkregen voordat het eerste veld wordt geschreven, niet achteraf gevalideerd door een notificatiehook, dus een geweigerde mutatie laat het model byte-identiek achter. De regression suite assert precies dat, en leest bladnaam, zoom, zichtbaarheid, standaardhoogte, marges, oriëntatie en pagina-eindtellingen opnieuw na elke geweigerde aanroep. Laadpaden krijgen dezelfde behandeling één laag lager, waar de ZIP read gate concurrente inflate coördineert voor pakketformaten

procedure TXLSWorksheet.Activate;
var
  WriteGuard: IXLSWorkbookWriteGuard;
begin
  // Verkregen voordat het eerste veld wordt aangeraakt, nooit erna
  WriteGuard := FWorkbook._AcquireWriteGuard;
  if not FSelected then
  begin
    FWorkbook.FWorkSheets.Deselect;
    FSelected := True;
  end;
  FWorkbook.FWorkSheets.FActiveSheet := Self;
  // Alleen een voltooide buitenste guard schuift de generatie op
  WriteGuard.Complete;
end;

Waarom schuift een geneste schrijfactie de generatie maar één keer op?

Omdat een schrijftransactie wordt gedefinieerd door de buitenste guard op een thread, niet door elke guard afzonderlijk. De kern houdt een per-thread schrijverstatus bij met een thread-id, een diepte en een voltooiingsvlag. Een tweede AcquireWriteGuard op dezelfde thread vindt die status en verhoogt Depth in plaats van een nieuwe transactie te creëren, en pas wanneer Depth terugvalt naar nul — met de buitenste guard als Complete gemarkeerd — schuift FGeneration op. Dit is wat een high-level operatie zoals Calculate of Open toestaat tien begrende primitieven eronder aan te roepen en toch als één wijziging te registreren. Binnenste Complete-aanroepen worden vastgelegd maar verplaatsen de teller niet op eigen kracht, en de guards mogen uit volgorde worden vrijgegeven zonder de boekhouding te breken

De faalrichting is even expliciet. Als een guard wordt vrijgegeven zonder Complete — het gewone gevolg van een exception die de interfacereferentie afwikkelt — schuift de generatie niet op, want de schrijftransactie heeft nooit succes geclaimd. Wees nuchter over wat dat betekent: HotXLS rolt de gedeeltelijke bewerking niet terug. De teller legt vast dat geen succesvolle transactie voltooide, precies het signaal dat een cache nodig heeft, maar het model terugbrengen naar zijn vorige status is niet iets dat een referentietellende guard voor u kan doen. Als een mislukking halverwege de transactie de werkmap in een vorm kan achterlaten die u niet kunt uitleveren, bewaar dan het bronbestand en open het opnieuw, in plaats van het in-memory object te vertrouwen

Twee HotXLS-schrijftransactietijdlijnen vergeleken: geneste guards op één thread verhogen de diepte en schuiven de generatieteller alleen op wanneer de buitenste guard voltooit, terwijl een exception die de guards afwikkelt zonder Complete de generatie onveranderd laat en de gedeeltelijke bewerking op zijn plaats laat
Depth volgt de nesteling, maar alleen een voltooide buitenste transactie schuift de generatie op, en een afgebroken transactie laat zowel de teller als de gedeeltelijke bewerking precies waar ze waren

Wat de generatieteller u oplevert

Goedkope verouderingsdetectie zonder scannen. Generation is een UInt64 die bij 1 begint en 0 overslaat bij omloop, dus 0 is nooit een waarde die de kern uitgeeft en werkt als betrouwbare nooit-waargenomen-sentinel. Twee invarianten maken haar bruikbaar: de generatie kan niet bewegen zolang een read lease bestaat, en elke succesvolle schrijftransactie verhoogt haar precies één keer. IXLSWorkbookReadLease.Generation is dus een momentopname die constant blijft gedurende het hele leven van de lease, en IXLSWorkbookWriteGuard.StartGeneration vertelt een schrijver hoe het model eruitzag toen zijn transactie opende. Een grid, een afdrukvoorbeeld of een afgeleide index kan één integer vergelijken in plaats van rijen te diffen

var
  Lease: IXLSWorkbookReadLease;
begin
  Lease := FWorkbook._AcquireReadLease;
  if Lease.Generation <> FCachedGeneration then
  begin
    FCachedGeneration := Lease.Generation;
    RebuildRowHeightCache;
  end;
  PaintVisibleRows;
  // FCachedGeneration begint bij 0, een waarde de kern nooit uitgeeft,
  // dus de allereerste pass bouwt altijd opnieuw
end;

Wat deze coördinatie niet belooft

Drie limieten zijn de moeite van plain zeggen waard, want anders aannemen is hoe het mechanisme wordt misbruikt. Ten eerste is een write guard geen onderlinge uitsluiting tussen schrijvers: de kern sluit lezers uit tegen schrijvers, en twee verschillende threads kunnen elk tegelijk een write guard vasthouden, elk de generatie onafhankelijk opschuivend — een regression test assert precies dit gedrag. Uw eigen schrijverthreads serialiseren blijft uw taak. Ten tweede is hier niets een bestandslock of een cross-process mutex; het coördineert threads binnen één proces tegen één werkmapinstantie, en twee processen die dezelfde .xlsx openen weten niets van elkaar. Ten derde bereikt de garantie alleen aanroepers die daadwerkelijk een lease nemen — een leesactie zonder lease loopt nog steeds een onbegrend heet pad, dat snel en volledig onbeschermd is. Dit is een coördinatiekern, geen transactionele database

Binnen die grenzen gebruikt is het een kleine, eerlijke primitief: negen dedicated regression tests dekken meerdere lezers, beide conflictrichtingen, reentrancy, out-of-order vrijgave, afgebroken transacties en cross-thread lees/schrijf- en schrijf/schrijf-races, binnen een suite van 1.328 tests die slagen op Win32 en Win64. Combineer haar met het crash-safe pad voor gestaged opslaan in temp-bestanden en een achtergrondexport wordt iets waar u end-to-end over kunt redeneren — consistent tijdens het lezen, atomair tijdens het schrijven. Read leases, write guards en de generatieteller verschepen als onderdeel van de klassieke en pakket-engines in de HotXLS Delphi Component voor Delphi en C++Builder, zonder configuratie nodig om ze in te schakelen