HotXLS bevakar en arbetsbok på disk med TXLSFileMonitor och lyfter OnFileChanged när en annan process sparar över filen. Att sätta Monitor.Active := False inifrån den hanteraren brukade hänga i all evighet, för worker-tråden låg fortfarande blockerad och väntade på den callback den just hade skickat. Fixen är att låta trådobjektet bära identiteten på den callback det för närvarande kör
Varför hänger det att stoppa en filmonitor inuti sin egen hanterare?
För att avaktiveringsanropet och callbacken kör på två olika trådar, och var och en väntar på att den andra ska bli klar. TXLSFileMonitor levereras med SynchronizeEvent satt till True, vilket betyder att pollingtråden inte anropar din hanterare själv. Den anropar TThread.Synchronize, lämnar metoden till huvudtråden och blockerar sedan tills huvudtråden kört den och signalerat att den är klar. Det är precis vad du vill ha för en hanterare som laddar om ett rutnät eller målar om en form, och det är också det som gör det uppenbara avaktiveringsanropet till en fälla
procedure TMainForm.HandleFileChanged(Sender: TObject);
begin
// Vi är på huvudtråden här, tack vare TThread.Synchronize
FBook.LoadFromFile(FMonitor.FileName);
RefreshGrid;
// Användaren ville bara ha en omladdning, så sluta bevaka
FMonitor.Active := False; // brukade aldrig returnera
end;
Spåra de två stackarna så är cykeln uppenbar. Huvudtråden är inuti SetActive(False), som terminerar worker-tråden och sedan anropar Worker.WaitFor för att vara säker på att tråden verkligen har avslutats innan objektet frigörs. Worker-tråden är under tiden parkerad inuti TThread.Synchronize och väntar på att huvudtråden ska returnera från just den hanterare som anropar SetActive. Ingen sida kan röra sig. Applikationen kraschar inte, den höjer inget undantag, den slutar bara måla om, vilket är den värsta diagnostiska signatur en bugg kan ha
Samma dödläge bär ett andra ansikte
Att stänga av synkroniseringen undviker inte problemet, det flyttar bara på det. Med SynchronizeEvent satt till False — det förnuftiga valet i en tjänst eller ett konsolverktyg utan message loop — anropar TXLSFileMonitor hanteraren direkt på pollingtråden. Din hanterare kör nu på worker-tråden, så ett anrop till Monitor.Active := False inifrån den når Worker.WaitFor på själva worker-tråden. En tråd kan inte vänta på sin egen exit, så den blockerar tills något utifrån dödar processen. Båda varianterna är samma bugg: SetActive(False) antog att det alltid anropades från någonstans utanför callbacken. Det antagandet håller för en Close-knapp eller en formdestruktor och fallerar för den enda anropsplats folk sträcker sig efter först, vilket är varför den överlevde så länge. Varje komponent som både äger en tråd och skickar användarkod från den ärver samma risk, och lösningen är densamma i varje fall — teardown-vägen måste kunna avgöra om den körs inifrån en callback som tråden den tänker join:a fortfarande håller i
Låt worker-tråden bära callback-identiteten
HotXLS löser det genom att på själva trådobjektet registrera vilken OS-tråd som för närvarande exekverar hanteraren. TXLSFileMonitorThread.FireEvent stämplar FCallbackThreadID med GetCurrentThreadId omedelbart före det anropar hanteraren och rensar det i ett finally-block efteråt. Det enda fältet räcker för att avaktiveringsvägen ska kunna skilja "anropad från orelaterad kod" från "anropad inifrån min egen callback", och det fungerar identiskt för båda dispatch-lägena, för i det synkroniserade fallet rapporterar GetCurrentThreadId inuti FireEvent redan huvudtråden
procedure TXLSFileMonitorThread.FireEvent;
var
Owner: TXLSFileMonitor;
Handler: TNotifyEvent;
begin
// En köad callback kan skickas av WaitFor under avaktivering
if Terminated then
Exit;
Owner := FOwner;
Handler := Owner.FOnFileChanged;
if not Assigned(Handler) then
Exit;
FCallbackThreadID := GetCurrentThreadId;
try
Handler(Owner);
finally
// Hanteraren kan ha förstört Owner, men den här tråden lever vidare
FCallbackThreadID := 0;
end;
end;
Avaktiveringsgrenen i SetActive gör sedan tre saker i en medveten ordning. Den kopplar loss tråden från monitorn genom att rensa FThread först, så GetActive rapporterar False från det ögonblicket och ett reentrant Active := False inuti samma hanterare avslutas omedelbart i stället för att försöka riva ned tråden två gånger. Den anropar Terminate. Först därefter avgör den hur trådobjektet ska återtas
Worker := TXLSFileMonitorThread(FThread);
FThread := nil; // monitorn är inaktiv från denna punkt
Worker.Terminate;
FHasBaseline := False;
if Worker.FCallbackThreadID = GetCurrentThreadId then
// Både direkta och synkroniserade hanterare måste returnera innan worker-
// tråden kan avslutas; den frigör sig själv efter att callbacken rullats av
Worker.FreeOnTerminate := True
else
begin
Worker.WaitFor;
Worker.Free;
end;
När identiteten matchar väntar HotXLS aldrig. Det sätter FreeOnTerminate := True och returnerar, och låter hanteraren bli klar, Synchronize-anropet slutföras, Execute observera Terminated, och RTL frigöra trådobjektet när callbackstacken väl rullat av helt. När identiteten inte matchar — en Close-knapp, en destruktor, ett bakgrundsjobb — är den klassiska WaitFor plus Free fortfarande rätt sak, för den ger dig en hård garanti att ingen ytterligare callback kan avfyras efter att Active := False returnerar. Två exit-vägar, ett fält för att välja mellan dem. En konsekvens förtjänar att sägas rakt ut: på självfrigöringsvägen överlever worker-tråden hanteraren. Ingenting i FireEvents returväg får röra monitorobjektet igen, för hanteraren har rätt att ha frigjort det. Det är därför FireEvent cachar Owner och Handler i lokala variabler på förhand och bara rör sitt eget fält i finally-satsen. Ett förryckt FOwner.Something efter hanteraranropet skulle förvandla ett fixat dödläge till en use-after-free, vilket är ett strikt sämre utfall
Varför måste FireEvent kontrollera Terminated innan det gör något?
För att WaitFor inte är ett passivt väntande. På huvudtråden fortsätter RTL-implementationen av TThread.WaitFor att tömma synkroniseringskön medan den blockerar, just för att en tråd som parkerat i Synchronize ska kunna slutföra och avsluta i stället för att dödläsa mot avstängningen. Det är en funktion, och det betyder att en callback som köades mikrosekunder innan du anropade Active := False från en Close-knapp fortfarande kan skickas under teardownen, efter att monitorn redan släppt sin FThread-referens. Guardet överst i FireEvent gör den dispatchen till en no-op. Utan det kan en applikation som stänger sitt huvudfönster medan en sparning landar på den bevakade filen köra en omladdningshanterare mot halvt avvecklat tillstånd, och den resulterande access violationen ser inte alls ut som ett avstängningsordningsproblem. Om din egen komponent skickar arbete genom Synchronize generaliserar regeln: varje synkroniserad metod behöver en Terminated-kontroll som sin första sats, inte för att tråden kan vara borta utan för att kön överlever beslutet att sluta
Samma buggform i CSV-importvägen
Granskningen som gav monitorfixen blottade ett syskonfel i textimportören, och det är värt att läsa sida vid sida för att det underliggande misstaget är identiskt: en konfigurationsbit som konsumeras som om den vore scratch-tillstånd. TXLSCSVLoader exponerar en SkipRows-egenskap för bannerraderna som sitter ovanför den riktiga rubriken i exporterade rapporter. Parsern behöver en nedräkning medan den arbetar igenom de raderna, och originalimplementationen räknade ned på egenskapens bakomliggande fält själv. Importera en fil så blev SkipRows tyst noll; återanvänd samma loader för en andra fil och bannerraderna hamnade i rutnätet
Loader := TXLSCSVLoader.Create;
try
Loader.SkipRows := 2; // två bannerrader ovanför rubriken
Loader.ParseFile('jan-export.csv');
ReadGrid(Loader);
// SkipRows är fortfarande 2 här: FSkipRemaining återställs per parsning
Loader.ParseFile('feb-export.csv');
ReadGrid(Loader);
finally
Loader.Free;
end;
HotXLS behåller nu FSkipRows som den deklarerade inställningen och återställer en separat FSkipRemaining-räknare vid starten av varje parsning, både i den avgränsade och i den fasta breddvägen. Den generella regeln är billig att tillämpa och betalar sig själv: om en loop dekrementerar den, är den inte ett egenskapsbakomliggande fält. Samma batch skärpte också XlsCsvTryParseISODateTime, som tidigare var villig att acceptera ett parsningsbart prefix som ett helt datum. Det validerar nu hela fältet — längd, avgränsningspositioner och varje siffra — så att ett värde som en orderreferens som bara börjar med något datumliknande förblir text i stället för att bli ett felaktigt TDateTime. Tyst typkoercion vid import är långt dyrare att felsöka nedströms än ett avvisat fält, ett tema som också löper genom hur HotXLS skyddar en arbetsbok mot samtidiga läsare och skribenter
Vad den här fixen ger dig och inte ger dig
Var precis om garantin. Trådägarskapsändringen gör exakt ett tidigare fatalt mönster säkert: att avaktivera eller frigöra en TXLSFileMonitor inifrån sin egen OnFileChanged-hanterare, i endera dispatch-läget. Det är inte en generell konkurrensdesign, och det gör inte komponenten säker att driva från godtyckliga trådar
- Egenskapskrivningar är fortfarande inte trådsäkra. Sätt
FileName,IntervalMsochSynchronizeEventfrån en tråd, normalt den som skapade monitorn IntervalMsgäller från nästa aktivering, och worker-tråden sover i korta skivor så attTerminatehedras snabbt även med en lång pollperiod- Detektering sker via senaste skrivtid plus storlek, så en ändring som bevarar båda är osynlig; en fil som försvinner mitt i en sparning avfyrar inget, och händelsen kommer när det nya innehållet är på plats
- Med
SynchronizeEventsatt tillFalsekörs din hanterare på worker-tråden, och varje regel om att röra VCL-kontroller från en bakgrundstråd gäller för allt du gör inuti den - Att ladda om en arbetsbok från hanteraren kräver fortfarande sin egen diciplin, särskilt om annan kod håller en referens till boken som ersätts
I praktiken är omladdningshanteraren den del som förtjänar mest omsorg, inte monitorn. En ändringshändelse anländer oftast medan den skrivande processen fortfarande håller på att landa, så en laddning som öppnar filen omedelbart kan möta en delvis genomförd sparning; att försöka en gång till efter en kort fördröjning är robustare än att lita på den första stämpeln. Är det din egen kod som skriver den bevakade filen ger den stegade tempfilen och atomiska renamingen som beskrivs i hur HotXLS gör varje sparning kraschsäker också monitorn en ren enkel-övergångshändelse att observera i stället för en ström av mellanstorlekar. Och om flera bevakare matar samma omladdningsrutin är serialiseringsfrågorna i lässpärren kring samtidig ZIP-inflation nästa sak att läsa. Den bredare läxan överlever den specifika komponenten. Varje klass som äger en tråd och anropar tillbaka in i användarkod behöver en teardown-väg som kan känna igen sin egen callback-ram, för hanteraren är världens mest naturliga plats för en användare att säga stopp. Att registrera callback-tråd-id:t kostar en DWORD och förvandlar en oförklarlig hängning till en vanlig objektlivstidsfråga. Filbevakning, atomiska sparningar och textimport levereras alla i HotXLS Delphi Component för Delphi och C++Builder, utan extra konfiguration för att aktivera något av det