HotXLS overvåger en workbook på disk med TXLSFileMonitor og udløser OnFileChanged når en anden proces gemmer over filen. At sætte Monitor.Active := False inde fra den handler plejede at hænge i det uendelige, fordi worker-tråden stadig blokerede og ventede på den callback den lige havde dispatchet. Rettelsen er at lade tråd-objektet bære identiteten på den callback det kører lige nu
Hvorfor hænger det at stoppe en filmonitor inde i dens egen handler?
Fordi deaktiveringskaldet og callback'en kører på to forskellige tråde, og hver af dem venter på at den anden bliver færdig. TXLSFileMonitor leveres med SynchronizeEvent sat til True, hvilket betyder at polling-tråden ikke kalder din handler selv. Den kalder TThread.Synchronize, overgiver metoden til hovedtråden og blokerer derefter indtil hovedtråden har kørt den og signaleret afslutning. Det er præcis hvad du vil have til en handler der genindlæser et grid eller repainter en form — og det er også det der gør det oplagte deaktiveringskald til en fælde
procedure TMainForm.HandleFileChanged(Sender: TObject);
begin
// Vi er på hovedtråden her, takket være TThread.Synchronize
FBook.LoadFromFile(FMonitor.FileName);
RefreshGrid;
// Brugeren ville kun have én genindlæsning, så stop overvågningen
FMonitor.Active := False; // plejede aldrig at returnere
end;
Følger du de to stacks, er cyklussen tydelig. Hovedtråden er inde i SetActive(False), som terminerer worker'en og derefter kalder Worker.WaitFor for at være sikker på at tråden virkelig er afsluttet før objektet frigives. Worker-tråden står i mellemtiden parkeret inde i TThread.Synchronize og venter på at hovedtråden returnerer fra netop den handler der kalder SetActive. Ingen af siderne kan bevæge sig. Applikationen crasher ikke, den raiser ikke, den stopper bare med at repainte — hvilket er den værste diagnostiske signatur en fejl kan have
Den samme deadlock har et andet ansigt
At slå synkroniseringen fra løser ikke problemet, det flytter bare rundt på det. Med SynchronizeEvent sat til False — det fornuftige valg i en service eller et konsolværktøj uden message loop — invokerer TXLSFileMonitor handleren direkte på polling-tråden. Din handler kører nu på worker'en, så et kald til Monitor.Active := False inde fra den når Worker.WaitFor på selve worker-tråden. En tråd kan ikke vente på sin egen afslutning, så den blokerer indtil noget udenfor dræber processen. Begge varianter er den samme fejl: SetActive(False) antog at den altid blev kaldt fra et sted uden for callback'en. Den antagelse holder for en Close-knap eller en form-destructor og fejler for det ene kaldested folk griber til først — hvilket er hvorfor den overlevede så længe. Enhver komponent der både ejer en tråd og dispatcher brugerkode fra den, arver den samme fare, og løsningen er den samme i alle tilfælde — teardown-stien skal kunne afgøre om den kører inde fra en callback som den tråd den er ved at joine, stadig holder på
Lad worker-tråden bære callback-identiteten
HotXLS løser det ved, på selve tråd-objektet, at registrere hvilken OS-tråd der lige nu udfører handleren. TXLSFileMonitorThread.FireEvent stempler FCallbackThreadID med GetCurrentThreadId umiddelbart før den invokerer handleren og rydder den i en finally-blok bagefter. Det ene felt er nok til at deaktiveringsstien kan skelne "kaldt fra ikke-relateret kode" fra "kaldt inde fra min egen callback", og det virker identisk for begge dispatch-tilstande, for i det synkroniserede tilfælde rapporterer GetCurrentThreadId inde i FireEvent allerede hovedtråden
procedure TXLSFileMonitorThread.FireEvent;
var
Owner: TXLSFileMonitor;
Handler: TNotifyEvent;
begin
// En køet callback kan blive dispatchet af WaitFor under deaktivering
if Terminated then
Exit;
Owner := FOwner;
Handler := Owner.FOnFileChanged;
if not Assigned(Handler) then
Exit;
FCallbackThreadID := GetCurrentThreadId;
try
Handler(Owner);
finally
// Handleren kan have ødelagt Owner, men denne tråd lever videre
FCallbackThreadID := 0;
end;
end;
Deaktiveringsgrenen i SetActive gør så tre ting i en bevidst rækkefølge. Den frigør tråden fra monitoren ved at rydde FThread først, så GetActive rapporterer False fra det øjeblik og frem, og et reentrant Active := False inde i den samme handler afsluttes øjeblikkeligt i stedet for at forsøge at rive tråden ned to gange. Den kalder Terminate. Først derefter beslutter den hvordan tråd-objektet skal geninddrives
Worker := TXLSFileMonitorThread(FThread);
FThread := nil; // monitoren er inaktiv fra dette punkt
Worker.Terminate;
FHasBaseline := False;
if Worker.FCallbackThreadID = GetCurrentThreadId then
// Både direkte og synkroniserede handlers skal returnere før worker'en
// kan afsluttes; den frigiver sig selv efter callback'en har rullet op
Worker.FreeOnTerminate := True
else
begin
Worker.WaitFor;
Worker.Free;
end;
Når identiteten matcher, venter HotXLS aldrig. Den sætter FreeOnTerminate := True og returnerer, lader handleren blive færdig, Synchronize-kaldet fuldføre, Execute observere Terminated, og RTL'en frigive tråd-objektet når callback-stacken er fuldt rullet op. Når identiteten ikke matcher — en Close-knap, en destructor, et baggrundsjobs — er den klassiske WaitFor plus Free stadig det rigtige, for den giver dig en hård garanti for at ingen yderligere callback kan afgå efter Active := False returnerer. To exit-stier, ét felt til at vælge mellem dem. Én konsekvens fortjener at blive sagt højt: på selvfrigivelses-stien overlever worker'en handleren. Intet i returstien for FireEvent må røre monitor-objektet igen, for handleren har ret til at have frigivet det. Det er derfor FireEvent cacher Owner og Handler ind i lokaler fra starten og kun rører sit eget felt i finally-klausulen. Et forvildet FOwner.Something efter handler-kaldet ville forvandle en rettet deadlock til en use-after-free, hvilket er et strengt værre udfald
Hvorfor skal FireEvent tjekke Terminated før den gør noget?
Fordi WaitFor ikke er en passiv ventetid. På hovedtråden bliver RTL-implementationen af TThread.WaitFor ved med at tømme synkroniseringskøen mens den blokerer — netop for at en tråd parkeret i Synchronize kan blive færdig og afslutte i stedet for at deadlocke mod nedlukningen. Det er en feature, og det betyder at en callback der blev køet mikrosekunder før du kaldte Active := False fra en Close-knap, stadig kan blive dispatchet under teardown'en — efter monitoren allerede har droppet sin FThread-reference. Vagten i toppen af FireEvent gør det dispatch til en no-op. Uden den kan en applikation der lukker sin hovedform mens en gem lander på den overvågede fil, køre en genindlæsningshandler mod halvt afviklet state, og den resulterende access violation ligner intet nedlukningsrækkefølge-problem. Hvis din egen komponent dispatcher arbejde gennem Synchronize, generaliserer reglen: hver synkroniseret metode har brug for et Terminated-tjek som sin første statement — ikke fordi tråden måske er væk, men fordi køen overlever beslutningen om at stoppe
Samme fejlform i CSV-importstien
Auditten der producerede monitor-rettelsen fandt en søster-fejl i tekst-importøren, og den er værd at læse side om side fordi den underliggende fejl er identisk: et stykke konfiguration der blev forbrugt som var det scratch-state. TXLSCSVLoader eksponerer en SkipRows-property for banner-linjerne der sidder over den rigtige header i eksporterede rapporter. Parseren har brug for en countdown mens den arbejder sig gennem de linjer, og den oprindelige implementation talte ned på selve property-backing-feltet. Importer én fil, og SkipRows blev tavst nul; genbrug den samme loader til en anden fil, og banner-linjerne landede i griddet
Loader := TXLSCSVLoader.Create;
try
Loader.SkipRows := 2; // to banner-linjer over headeren
Loader.ParseFile('jan-export.csv');
ReadGrid(Loader);
// SkipRows er stadig 2 her: FSkipRemaining nulstilles per parse
Loader.ParseFile('feb-export.csv');
ReadGrid(Loader);
finally
Loader.Free;
end;
HotXLS beholder nu FSkipRows som den deklarerede indstilling og nulstiller en separat FSkipRemaining-tæller ved starten af hver parse, i både den delimiterede og den fastbredde-sti. Den generelle regel er billig at anvende og betaler sig selv: hvis en løkke dekrementerer den, er det ikke et property-backing-felt. Samme batch strammede også XlsCsvTryParseISODateTime, som tidligere var villig til at acceptere et parserbart præfiks som en hel dato. Den validerer nu hele feltet — længde, separator-positioner og hvert ciffer — så en værdi som en ordrereference der blot begynder med noget dato-formet, forbliver tekst i stedet for at blive en forkert TDateTime. Tavs type-koercion ved import er langt dyrere at debugge nedstrøms end et afvist felt — et tema der også løber gennem hvordan HotXLS beskytter en workbook mod samtidige læsere og skrivere
Hvad denne rettelse giver dig — og ikke giver dig
Vær præcis om garantien. Trådejerskabs-ændringen gør præcis ét tidligere fatalt mønster sikkert: at deaktivere eller frigøre en TXLSFileMonitor inde fra dens egen OnFileChanged-handler, i begge dispatch-tilstande. Det er ikke et generelt concurrency-design, og det gør ikke komponenten sikker at drive fra vilkårlige tråde
- Property-skrivninger er stadig ikke thread-safe. Sæt
FileName,IntervalMsogSynchronizeEventfra én tråd, normalt den der oprettede monitoren IntervalMsgælder fra næste aktivering, og worker'en sover i korte bidder såTerminateefterleves hurtigt selv med en lang poll-periode- Detektion sker via sidste-skrivning-tidspunkt plus størrelse, så en ændring der bevarer begge er usynlig; en fil der forsvinder midt i en gem afgår ikke, og hændelsen kommer når det nye indhold er på plads
- Med
SynchronizeEventsat tilFalsekører din handler på worker-tråden, og alle regler om at røre VCL-kontroller fra en baggrundstråd gælder for hvad end du laver inde i den - At genindlæse en workbook fra handleren kræver stadig sin egen disciplin, især hvis anden kode holder en reference til den bog der erstattes
I praksis er genindlæsningshandleren den del der fortjener mest omhu, ikke monitoren. En ændringshændelse kommer som regel mens skriveprocessen stadig er ved at falde til ro, så en indlæsning der åbner filen med det samme kan møde en delvist forpligtet gem; at prøve igen én gang efter en kort forsinkelse er mere robust end at stole på det første stempel. Hvis din egen kode er den der skriver den overvågede fil, giver den stagede temp-fil og det atomare rename beskrevet i hvordan HotXLS gør hver gem crash-sikker også monitoren en ren enkelt-transitions-hændelse at observere frem for en strøm af mellemliggende størrelser. Og hvis flere watchers fodrer den samme genindlæsningsrutine, er serialiseringsbekymringerne i read-gaten omkring samtidig ZIP-inflation det næste at læse. Den bredere lektion overlever den specifikke komponent. Enhver klasse der ejer en tråd og kalder tilbage ind i brugerkode, har brug for en teardown-sti der kan genkende sin egen callback-frame, for handleren er det mest naturlige sted i verden for en bruger at sige stop. At registrere callback-tråd-id'et koster én DWORD og forvandler en uforklarlig hang til et almindeligt objekt-levetids-spørgsmål. Filovervågning, atomare gems og tekstimport leveres alle i HotXLS Delphi Component til Delphi og C++Builder, uden ekstra konfiguration for at aktivere nogen af delene