HotXLS hlídá workbook na disku přes TXLSFileMonitor a zvedá OnFileChanged, když jiný proces uloží přes soubor. Nastavení Monitor.Active := False zevnitř toho handleru dřív viselo navždy, protože worker vlákno pořád blokovalo na čekání na callback, který právě rozeslalo. Oprava spočívá v tom, že thread objekt ponese identitu callbacku, který právě běží
Proč zastavení file monitoru uvnitř vlastního handleru visí?
Protože volání deaktivace a callback běží na dvou různých vláknech a každé čeká, až to druhé skončí. TXLSFileMonitor se dodává s SynchronizeEvent nastaveným na True, což znamená, že polling vlákno váš handler nevolá samo. Zavolá TThread.Synchronize, předá metodu hlavnímu vláknu a pak blokuje, dokud hlavní vlákno metodu nespusí a neohlásí dokončení. Přesně to chcete pro handler, který znovu načte grid nebo překreslí form, a přesně to dělá z nasnadě ležícího volání deaktivace past
procedure TMainForm.HandleFileChanged(Sender: TObject);
begin
// Jsme na hlavním vlákně, díky TThread.Synchronize
FBook.LoadFromFile(FMonitor.FileName);
RefreshGrid;
// Uživatel chtěl jen jedno znovunačtení, tak přestaň hlídat
FMonitor.Active := False; // kdysi se nikdy nevrátilo
end;
Vysledujte ty dva stacky a cyklus je jasný. Hlavní vlákno je uvnitř SetActive(False), které ukončí worker a pak zavolá Worker.WaitFor, aby si bylo jisté, že vlákno opravdu skončilo, než se objekt uvolní. Worker vlákno mezitím parkuje uvnitř TThread.Synchronize a čeká, až se hlavní vlákno vrátí z přesně toho handleru, který volá SetActive. Ani jedna strana se nehne. Aplikace nespadne, nevyhodí výjimku, prostě přestane překreslovat, což je nejhorší diagnostický podpis, jaký chyba může mít
Stejný deadlock má i druhou tvář
Vypnutí synchronizace problém nevyřeší, jen ho přemístí. S SynchronizeEvent nastaveným na False — rozumná volba ve službě nebo konzolovém nástroji bez message loop — volá TXLSFileMonitor handler přímo na polling vlákně. Váš handler teď běží na workeru, takže volání Monitor.Active := False zevnitř dorazí k Worker.WaitFor na samotném worker vlákně. Vlákno nemůže čekat na vlastní exit, takže blokuje, dokud něco zvenku proces nezabije. Obě varianty jsou stejná chyba: SetActive(False) předpokládalo, že se vždy volá odněkud mimo callback. Ten předpoklad platí pro Close tlačítko nebo destruktor formu a selhává u jediného call site, po kterém lidi sáhnou jako první, a proto tak dlouho přežil. Jakákoli komponenta, která vlastní vlákno a z něj rozesílá uživatelský kód, dědí stejné riziko, a řešení je v každém případě stejné — teardown cesta musí umět poznat, jestli běží zevnitř callbacku, který vlákno, jež se chystá připojit, pořád drží
Nechte worker vlákno nést identitu callbacku
HotXLS to řeší zaznamenáním, přímo na thread objektu, které OS vlákno právě vykonává handler. TXLSFileMonitorThread.FireEvent otiskne do FCallbackThreadID hodnotu GetCurrentThreadId těsně před vyvoláním handleru a v finally bloku ji potom vymaže. To jediné pole stačí, aby cesta deaktivace rozlišila "voláno z nesouvisejícího kódu" od "voláno zevnitř vlastního callbacku", a funguje identicky v obou režimech odesílání, protože v synchronizovaném případě GetCurrentThreadId uvnitř FireEvent už hlásí hlavní vlákno
procedure TXLSFileMonitorThread.FireEvent;
var
Owner: TXLSFileMonitor;
Handler: TNotifyEvent;
begin
// Zařazený callback může WaitFor rozeslat během deaktivace
if Terminated then
Exit;
Owner := FOwner;
Handler := Owner.FOnFileChanged;
if not Assigned(Handler) then
Exit;
FCallbackThreadID := GetCurrentThreadId;
try
Handler(Owner);
finally
// Handler mohl zničit Owner, ale tohle vlákno zůstává naživu
FCallbackThreadID := 0;
end;
end;
Větev deaktivace v SetActive pak dělá tři věci v promyšleném pořadí. Nejdřív odpojí vlákno od monitoru vyčištěním FThread, takže GetActive od toho okamžiku hlásí False a reentrantní Active := False uvnitř téhož handleru rovnou skončí, místo aby se vlákno pokoušelo rozebrat dvakrát. Zavolá Terminate. A teprve pak se rozhodne, jak thread objekt uvolnit
Worker := TXLSFileMonitorThread(FThread);
FThread := nil; // monitor je od tohoto bodu neaktivní
Worker.Terminate;
FHasBaseline := False;
if Worker.FCallbackThreadID = GetCurrentThreadId then
// Přímé i synchronizované handlery se musí vrátit, než worker
// může skončit; uvolní se sám, až se callback rozvine
Worker.FreeOnTerminate := True
else
begin
Worker.WaitFor;
Worker.Free;
end;
Když se identita shoduje, HotXLS nikdy nečeká. Nastaví FreeOnTerminate := True a vrátí se, nechá handler doběhnout, volání Synchronize dokončit, Execute zpozorovat Terminated a RTL uvolnit thread objekt, jakmile se callback stack plně rozvine. Když se identita neshoduje — Close tlačítko, destruktor, úloha na pozadí — je klasické WaitFor plus Free pořád to správné, protože dává tvrdou záruku, že po návratu z Active := False už nemůže vystřelit žádný další callback. Dvě exit cesty, jedno pole, které mezi nimi volí. Jeden důsledek si zaslouží být vysloven: na cestě samovolného uvolnění worker přežije handler. Nic v návratové cestě FireEvent se nesmí znovu dotknout objektu monitoru, protože handler ho mohl oprávněně uvolnit. Proto si FireEvent ukládá Owner a Handler do lokálních proměnných dopředu a v finally klauzuli se dotýká jen vlastního pole. Zatoulané FOwner.Something po volání handleru by proměnilo opravený deadlock v use-after-free, což je přísně horší výsledek
Proč musí FireEvent kontrolovat Terminated, než cokoli udělá?
Protože WaitFor není pasivní čekání. Na hlavním vlákně RTL implementace TThread.WaitFor za běhu odčerpává synchronizační frontu, přesně proto, aby vlákno parkující v Synchronize mohlo dokončit a skončit, místo aby deadlockovalo proti vypínání. To je vlastnost, a znamená to, že callback zařazený mikrosekundy předtím, než jste z Close tlačítka zavolali Active := False, může být pořád rozeslán během teardownu, poté, co monitor už upustil referenci FThread. Stráž na začátku FireEvent dělá z toho rozeslání no-op. Bez ní může aplikace, která zavírá hlavní form, zatímco na hlídaný soubor dopadá uložení, spustit reload handler proti napůl zlikvidovanému stavu, a výsledný access violation nevypadá vůbec jako problém pořadí vypínání. Pokud vaše komponenta rozesílá práci přes Synchronize, pravidlo zobecňuje: každá synchronizovaná metoda potřebuje kontrolu Terminated jako první příkaz, ne proto, že vlákno může být pryč, ale proto, že fronta přežije rozhodnutí zastavit
Stejný tvar chyby v cestě CSV importu
Audit, který vyprodukoval opravu monitoru, odhalil sourozeneckou vadu v textovém importéru, a stojí za to ji číst vedle té první, protože základní chyba je identická: kus konfigurace konzumovaný, jako by to byl scratch stav. TXLSCSVLoader vystavuje vlastnost SkipRows pro bannerové řádky, které v exportovaných reportech sedí nad skutečnou hlavičkou. Parser potřebuje odpočet, dokud ty řádky prochází, a původní implementace odpočítávala přímo na backing poli vlastnosti. Naimportujte jeden soubor a SkipRows tiše bylo nula; použijte stejný loader pro druhý soubor a bannerové řádky přistály v gridu
Loader := TXLSCSVLoader.Create;
try
Loader.SkipRows := 2; // dva bannerové řádky nad hlavičkou
Loader.ParseFile('jan-export.csv');
ReadGrid(Loader);
// SkipRows je tady pořád 2: FSkipRemaining se resetuje při každém parsu
Loader.ParseFile('feb-export.csv');
ReadGrid(Loader);
finally
Loader.Free;
end;
HotXLS teď drží FSkipRows jako deklarované nastavení a resetuje samostatný čítač FSkipRemaining na začátku každého parsu, v obou cestách, oddělovačové i pevné šířky. To obecné pravidlo je levné a samo se zaplatí: když to smyčka dekrementuje, není to backing pole vlastnosti. Stejná dávka zpřísnila i XlsCsvTryParseISODateTime, která dřív byla ochotná přijmout parsovatelný prefix jako celé datum. Teď validuje celé pole — délku, pozice oddělovačů i každou číslici — takže hodnota, třeba reference objednávky, která jen začíná něčím ve tvaru data, zůstane textem místo toho, aby se stala špatným TDateTime. Tichá typová koerce při importu je dole ve pipeline mnohem dražší na odladění než odmítnuté pole, téma, které probíhá i tím, jak HotXLS chrání workbook před souběžnými čtenáři a zapisovači
Co tahle oprava dává a nedává
Buďte přesní v záruce. Změna vlastnictví vlákna činí bezpečný přesně jeden dříve fatální vzor: deaktivaci nebo uvolnění TXLSFileMonitor zevnitř vlastního handleru OnFileChanged, v obou režimech odesílání. Není to obecný návrh souběžnosti a nedělá z komponenty něco, co je bezpečné řídit z libovolných vláken
- Zápisy vlastností pořád nejsou thread-safe.
FileName,IntervalMsaSynchronizeEventnastavujte z jednoho vlákna, obvykle z toho, které monitor vytvořilo IntervalMsplatí od příští aktivace a worker spí v krátkých úsecích, takžeTerminatese ctí promptně i s dlouhou periodou pollingu- Detekce jde po čase posledního zápisu plus velikosti, takže změna zachovávající obojí je neviditelná; soubor, který zmizí uprostřed uložení, nevystřelí, a event dorazí, až je nový obsah na místě
- S
SynchronizeEventnastaveným naFalseběží váš handler na worker vlákně a každé pravidlo o dotýkání se VCL ovládacích prvků z vlákna na pozadí platí i pro to, co děláte uvnitř - Znovunačtení workbooku z handleru pořád potřebuje vlastní disciplínu, hlavně když jiný kód drží referenci na nahrazovaný book
V praxi si nejvíc péče zaslouží reload handler, ne monitor. Změnová událost obvykle dorazí, když se zapisující proces ještě usazuje, takže načtení, které soubor otevře okamžitě, může narazit na napůl zapsané uložení; jeden retry po krátké prodlevě je odolnější než věřit prvnímu časovému razítku. Pokud hlídaný soubor zapisuje váš vlastní kód, fáze dočasného souboru a atomický rename popsané v tom, jak HotXLS dělá každé uložení crash-safe, dají monitoru taky čistou jednu přechodovou událost k pozorování místo proudu mezivelikostí. A pokud do stejné reload rutiny krmí víc hlídačů, serializační starosti z read gate kolem souběžné ZIP inflace jsou další věc ke čtení. Širší poučení přežije konkrétní komponentu. Každá třída, která vlastní vlákno a volá zpět do uživatelského kódu, potřebuje teardown cestu, která pozná vlastní callback frame, protože handler je to nejpřirozenější místo na světě, kde uživatel řekne stop. Zaznamenání id vlákna callbacku stojí jeden DWORD a mění nevysvětlitelné visení v obyčejnou otázku životnosti objektu. File monitoring, atomická uložení a textový import se všechny dodávají v HotXLS Delphi Component pro Delphi a C++Builder, bez jakékoli extra konfigurace