En bakgrundstråd exporterade en rapport på 40 000 rader när UI-tråden satte en enda cell, och filen som hamnade på disk matchade ingen arbetsbok som någonsin existerat. HotXLS hanterar den klassen av buggar i lxWorkbookView.pas, där IXLSWorkbookViewCore delar ut O(1) läs-lån och fail-fast skriv-vakter: medan ett lån är öppet kastar varje mutationsingångspunkt ett undantag i stället för att skriva
Felet som anländer utan stackspårning
Att läsa en arbetsbok är aldrig en atomär operation. En rapportgenomgång är tiotusentals individuella celläsningar utspridda över sekunder, och en enda SetValue som landar mellan två av dem räcker för att förändra vad resten av genomgången ser. Den klassiska motorn gör det konkret: TXLSCellRef.SetValue kan anropa FSST.Remove för att släppa en delad strängpost, nollställa FValueType och ogiltigförklara ett formelcache-tillstånd, allt medan en annan tråd är mitt uppe i att avreferera just de strukturerna. Inget kraschar på fläcken. Du får en rapport vars delsummor inte går ihop, eller en export som tyst läser ett strängindex som nu pekar någon annanstans
HotXLS löser det avsiktligt inte genom att låta skrivare vänta. En läsare kan hålla en arbetsbok i flera sekunder, och i en VCL-applikation är skrivaren ofta en UI-callback eller en händelsehanterare på huvudtråden — att blockera den tråden tills en bakgrundsexport blir klar är ett sämre utfall än att låta redigeringen misslyckas. Därför kastar samordningskärnan EXLSWorkbookWriteGuardUnavailable i samma ögonblick som en skrivning försöks mot ett öppet lån, innan ett enda fält har rörts, och anroparen avgör om redigeringen ska köas, göras om eller meddelas användaren. Fail-fast-konflikter, inte köade
Är en arbetsbok säker att läsa från två trådar?
Ja, förutsatt att båda läsarna håller ett lån och ingen skriver. IXLSWorkbookViewCore.AcquireReadLease tar en TCriticalSection, inkrementerar en räknare, tar en ögonblicksbild av den aktuella generationen och returnerar ett IXLSWorkbookReadLease — konstant tid oavsett om arbetsboken håller tusen celler eller en miljon. Hur många lån som helst samexisterar, de får släppas i valfri ordning, och vart och ett håller kärnan vid liv genom sin egen interfacereferens, så ett lån som överlever objektet som skapade det är säkert snarare än en hängande pekare. Båda motorerna deltar: TXLSWorkbook i lxHandle.pas och TXLSXWorkbook i lxHandleX.pas bygger varsin kärna i sin konstruktor och exponerar _AcquireReadLease och _AcquireWriteGuard
Det som spelar lika stor roll är vad lånet inte lägger till i läsvägen. Den kritiska sektionen täcker låneförvärv, lånesläpp och skrivtransaktionsgränser — inget annat. Den vanliga cell-för-cell-läsningen går aldrig in i ett lås, en monitor eller en atomär räknare, så att hålla ett lån kostar ett förvärv och ett släpp för hela skanningen, inte ett per cell. Det är samma designinstinkt bakom arbetet med parallell XLSX-parsning och minnesallokerare: betala för samordning vid gränsen, aldrig i inre loopen. Den symmetriska regeln gäller också — AcquireReadLease kastar EXLSWorkbookReadLeaseUnavailable närhelst WriteDepth är nollskild, så du kan inte öppna ett lån inifrån en skrivtransaktion, inte ens på skrivartråden
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// Kastar EXLSWorkbookReadLeaseUnavailable om en skrivning är på väg
Lease := FWorkbook._AcquireReadLease;
Sheet := FWorkbook.Sheets[1];
Total := 0;
for Row := 1 to 50000 do
Total := Total + Sheet.Cells[Row, 3].Value;
FTotal := Total;
// Lånet lämnar scope här: dess referensantal faller till noll,
// ReleaseReadLease körs, och skrivare blir möjliga igen
end;
Var sitter skrivvakten egentligen?
Vid det lägre muterbara lagret, aldrig vid bekvämlighets-API:t ovanpå det. _AcquireWriteGuard anropas inifrån själva TXLSCellRef.SetValue, vilket betyder att varje publik väg som rinner in i den — Range.Value, texttilldelning på kalkylark, cell-för-cell-kopiering, inklistring — grindas en gång i stället för att varje wrapper upprepar en kontroll som en framtida wrapper glömmer. Täckningen är avsiktligt bred: 55 vaktförvärv i lxHandle.pas och 37 i lxHandleX.pas i den batch som införde kärnan
Den grindade ytan spänner över cellvärden och cellformatering, TXLSWorkbook.Open, kopiering och inklistring, definierade namn (Add, döp om, RefersTo, Visible, IsMacro, Comment, Delete), kalkylarksmetadata som Name, Zoom, Visible, StandardHeight, FreezePanes, Protect och Activate, sidinställningar, sidbrytningar och Calculate. Placeringen är hela poängen: vakten förvärvas innan det första fältet skrivs, inte validerad i efterhand av en notifieringskrok, så en avvisad mutation lämnar modellen byte-identisk. Regressionssviten hävdar just det, genom att läsa tillbaka arknamn, zoom, synlighet, standardradhöjd, marginaler, orientering och antal sidbrytningar efter varje vägrat anrop. Laddningsvägar får samma behandling ett lager ner, där ZIP-läsgrinden samordnar samtidig inflate för paketformat
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Förvärvad innan det första fältet rörs, aldrig efter
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Endast en fullbordad yttersta vakt flyttar fram generationen
WriteGuard.Complete;
end;
Varför flyttar en nästlad skrivning fram generationen bara en gång?
Eftersom en skrivtransaktion definieras av den yttersta vakten på en tråd, inte av varje vakt för sig. Kärnan håller ett per-tråd-skrivartillstånd med ett tråd-id, ett djup och en slutförandeflagga. Ett andra AcquireWriteGuard på samma tråd hittar det tillståndet och inkrementerar Depth i stället för att skapa en ny transaktion, och först när Depth faller tillbaka till noll — med den yttersta vakten markerad Complete — flyttar FGeneration fram. Det är det som låter en högnivåoperation som Calculate eller Open anropa tio vaktade primitiver undertill och ändå registreras som en enda ändring. Inre Complete-anrop registreras men flyttar inte räknaren på egen hand, och vakterna får släppas i oordning utan att redovisningen bryts
Felriktningen är lika explicit. Om en vakt släpps utan Complete — den vanliga konsekvensen av att ett undantag varvar ner interfacereferensen — flyttar generationen inte fram, eftersom skrivtransaktionen aldrig gjorde anspråk på framgång. Var klarögd om vad det betyder: HotXLS rullar inte tillbaka den ofullständiga redigeringen. Räknaren noterar att ingen lyckad transaktion slutfördes, vilket är precis den signal ett cache behöver, men att återställa modellen till sitt tidigare tillstånd är inget en referensräknad vakt kan göra åt dig. Om ett fel mitt i transaktionen kan lämna arbetsboken i ett skick du inte kan skeppa, behåll källfilen och öppna den på nytt i stället för att lita på minnesobjektet
Vad generationsräknaren ger dig
Billig föråldradetektering utan någon skanning. Generation är en UInt64 som börjar på 1 och hoppar över 0 vid heltalsvarv, så 0 är aldrig ett värde kärnan delar ut och fungerar som en pålitlig ”aldrig observerad”-sentinel. Två invarianter gör den användbar: generationen kan inte röra sig medan något läs-lån existerar, och varje lyckad skrivtransaktion inkrementerar den exakt en gång. Så IXLSWorkbookReadLease.Generation är en ögonblicksbild som förblir konstant under hela lånets liv, och IXLSWorkbookWriteGuard.StartGeneration talar om för en skrivare hur modellen såg ut när dess transaktion öppnade. Ett rutnät, en utskriftsförhandsvisning eller ett härlett index kan jämföra ett heltal i stället för att diffa rader
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration börjar på 0, ett värde kärnan aldrig delar ut,
// så det allra första passet bygger alltid om
end;
Vad den här samordningen inte lovar
Tre gränser är värda att säga rent ut, eftersom att anta motsatsen är hur mekanismen missbrukas. För det första är en skrivvakt inte ömsesidig uteslutning mellan skrivare: kärnan utesluter läsare mot skrivare, och två olika trådar kan varsin hålla en skrivvakt samtidigt, var och en flyttande generationen oberoende — ett regressionstest hävdar just det beteendet. Att serialisera dina egna skrivartrådar är fortfarande ditt jobb. För det andra är inget här ett fillås eller en kors-process-mutex; den samordnar trådar inuti en process mot en arbetsboksinstans, och två processer som öppnar samma .xlsx vet ingenting om varandra. För det tredje når garantin bara anropare som faktiskt tar ett lån — en olånad läsning går fortfarande en olåst snabbväg, som är snabb och helt oskyddad. Det här är en samordningskärna, inte en transaktionsdatabas
Inom de gränserna är det en liten, ärlig primitiv: nio dedikerade regressionstester täcker flera läsare, båda konfliktriktningarna, återinträde, släpp i oordning, avbrutna transaktioner och kors-tråds-race av läs/skriv- och skriv/skriv-typ, i en svit på 1 328 tester som passerar på Win32 och Win64. Para den med den kraschsäkra sparvägen med iscensatta tempfiler blir en bakgrundsexport något du kan resonera om från början till slut — konsekvent medan den läser, atomär när den skriver. Läs-lån, skriv-vakter och generationsräknaren levereras som del av de klassiska motorerna och paketmotorerna i HotXLS Delphi Component för Delphi och C++Builder, utan någon konfiguration för att aktivera dem