Egy háttérszál 40 000 soros jelentést exportált, amikor a UI szál beállított egy cellát, és a lemezre került fájl egyetlen valaha létezett munkafüzettel sem egyezett. A HotXLS ezt a hibaosztályt a lxWorkbookView.pas-ban kezeli, ahol az IXLSWorkbookViewCore O(1) olvasási bérleményeket és gyors hibázású íróvédőket bocsát ki: amíg egy bérlemény nyitva áll, minden módosító belépési pont írás helyett kivételt dob
A hiba, amely veremnyom nélkül érkezik
Egy munkafüzet olvasása sosem egyetlen atomi művelet. Egy jelentésjárás több tízezer különálló cellaolvasás, másodpercekre szétszórva, és egyetlen SetValue, amely kettő közé landol, elég ahhoz, hogy megváltoztassa, mit lát a járás többi része. A klasszikus motor kézzelfoghatóvá teszi ezt: a TXLSCellRef.SetValue meghívhatja az FSST.Remove-ot egy megosztott sztring bejegyzés eldobására, alaphelyzetbe állíthatja a FValueType-ot, és érvényteleníthet egy képletgyorsítótár-állapotot, mindezt úgy, hogy egy másik szál épp azokat a struktúrákat dereferálja félúton. Semmi nem omlik össze a helyszínen. Egy jelentést kap, amelynek részösszegei nem adódnak össze, vagy egy exportot, amely csendben olyan sztringindexet olvas, amely most már máshová mutat
A HotXLS szándékosan nem úgy oldja meg ezt, hogy az írókat várakoztatja. Egy olvasó másodpercekig tarthat egy munkafüzetet, és egy VCL alkalmazásban az író gyakran UI visszahívás vagy eseménykezelő a főszálon — az a szál blokkolása, amíg egy háttér export befejeződik, rosszabb kimenet, mint a szerkesztés elbuktatása. Ezért a koordinációs mag abban a pillanatban dob EXLSWorkbookWriteGuardUnavailable-t, amikor egy írás nyitott bérleménnyel szemben kísérleti meg, mielőtt egyetlen mező hozzáért volna, és a hívó dönti el, hogy sorbaállítja a szerkesztést, újrapróbálja, vagy megmondja a felhasználónak. Gyorsan hibázó konfliktusok, nem sorbaállítottak
Biztonságos-e egy munkafüzetet két szálról olvasni?
Igen, feltéve hogy mindkét olvasó bérleményt tart, és senki sem ír. Az IXLSWorkbookViewCore.AcquireReadLease egy TCriticalSection-t foglal, növel egy számlálót, pillanatképet készít az aktuális generációról, és egy IXLSWorkbookReadLease-t ad vissza — konstans időben, függetlenül attól, hogy a munkafüzet ezer cellát vagy egymilliót tart. Bármennyi bérlemény együtt élhet, bármilyen sorrendben bocsáthatók ki, és mindegyik a saját interfészreferenciáján át élteti a magot, így egy a létrehozó objektumnál tovább élő bérlemény biztonságos, nem lógó pointer. Mindkét motor részt vesz: a lxHandle.pas TXLSWorkbook-ja és a lxHandleX.pas TXLSXWorkbook-ja egyaránt magot épít a konstruktorában, és _AcquireReadLease-t és _AcquireWriteGuard-ot tár fel
Ugyanannyira számít, amit a bérlemény nem ad hozzá az olvasási útvonalhoz. A kritikus szakasz a bérlemény megszerzését, a bérlemény kiadását és az írástranzakció határait fedi le — semmi mást. A közönséges cellánkénti olvasás sosem lép zárolásba, monitorba vagy atomi számlálóba, így egy bérlemény tartása egy megszerzést és egy kiadást fizet az egész pásztázásért, nem cellánkénti egyet. Ez ugyanaz a tervezési ösztön, amely a párhuzamos XLSX elemzés és memóriafoglaló munka mögött áll: a koordinációért a határon fizess, sosem a belső ciklusban. A szimmetrikus szabály is áll — az AcquireReadLease akkor is EXLSWorkbookReadLeaseUnavailable-t dob, amikor a WriteDepth nem nulla, így írástranzakción belülről nem nyithat bérleményt, az írószálon sem
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// EXLSWorkbookReadLeaseUnavailable-t dob, ha írás folyamatban van
Lease := FWorkbook._AcquireReadLease;
Sheet := FWorkbook.Sheets[1];
Total := 0;
for Row := 1 to 50000 do
Total := Total + Sheet.Cells[Row, 3].Value;
FTotal := Total;
// A bérlemény itt hagyja el a hatókört: hivatkozásszáma nullára csökken,
// a ReleaseReadLease lefut, és az írók ismét lehetővé válnak
end;
Hol ül valójában az íróvédő?
A legalacsonyabb módosítható rétegen, sosem a fölötte lévő kényelmi API-n. A _AcquireWriteGuard a TXLSCellRef.SetValue belsejéből hívódik, ami azt jelenti, hogy minden bele tölcséreződő nyilvános útvonal — Range.Value, munkalap szöveg hozzárendelés, cellánkénti másolás, beillesztés — egyszer kapuztatott, ahelyett hogy minden burkoló megismételne egy ellenőrzést, amelyet egy jövőbeli burkoló elfelejtene. A lefedettség szándékosan széles: 55 védőmegszerzés a lxHandle.pas-ban és 37 a lxHandleX.pas-ban a magot bevezető kötegnél
A kapuztatott felület átfogja a cellaértékeket és cellaformázást, a TXLSWorkbook.Open-t, másolást és beillesztést, a definiált neveket (Add, átnevezés, RefersTo, Visible, IsMacro, Comment, Delete), a munkalap metaadatokat, mint a Name, Zoom, Visible, StandardHeight, FreezePanes, Protect és Activate, az oldalbeállítást, az oldaltöréseket és a Calculate-t. Az elhelyezés a lényeg: a védő az első mező megírása előtt szereződik, nem utólag validálja egy értesítési horog, így egy elutasított módosítás bájtonként azonosan hagyja a modellt. A regressziós készlet pontosan ezt állítja, minden elutasított hívás után újraolvassa a munkalapnevet, zoomot, láthatóságot, standard magasságot, margókat, tájolást és oldaltörés-számokat. A betöltési útvonalak egy réteggel lejjebb ugyanígy járnak, ahol a ZIP olvasási kapu koordinálja a párhuzamos felfújást a csomagformátumokhoz
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Még az első mező érintése előtt szereződik, sosem utána
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Csak egy befejezett legkülső védő lépteti a generációt
WriteGuard.Complete;
end;
Miért lépteti a generációt egy beágyazott írás csak egyszer?
Mert az írástranzakciót a szál legkülső védője definiálja, nem minden védő külön-külön. A mag szálonkénti íróállapotot tart, amely szál-azonosítót, mélységet és befejezési jelzőt visel. Egy második AcquireWriteGuard ugyanazon a szálon megtalálja azt az állapotot, és a Depth-et növeli új tranzakció létrehozása helyett, és csak akkor lép a FGeneration, amikor a Depth nullára visszaesik — a legkülső védő Complete-ként megjelölve. Ez teszi lehetővé, hogy egy magas szintű művelet, mint a Calculate vagy az Open, tíz védett primitívet hívjon alatta, és mégis egyetlen változásként regisztráljon. A belső Complete hívások rögzítődnek, de maguktól nem mozgatják a számlálót, és a védők sorrenden kívül bocsáthatók ki a számadás megtörése nélkül
A hibairány ugyanilyen explicit. Ha egy védő Complete nélkül bocsátódik ki — egy kivétel visszatekercsének közönséges következménye, amely az interfészreferenciát bontja —, a generáció nem lép, mert az írástranzakció sosem követelt sikert. Lássuk tisztán, mit jelent ez: a HotXLS nem görgeti vissza a részleges szerkesztést. A számláló rögzíti, hogy egyetlen sikeres tranzakció sem fejeződött be, ami pontosan az a jel, amelyre egy gyorsítótárnak szüksége van, de a modell korábbi állapotra való visszállítása nem az, amit egy hivatkozásszámlált védő megtehet Önért. Ha egy tranzakció közbeni hiba olyan alakban hagyhatja a munkafüzetet, amelyet nem szállíthat, tartsa meg a forrásfájlt, és nyissa meg újra, a memóriabeli objektumba vetett bizalom helyett
Mit vásárol Önnek a generációs számláló
Olcsó elavultság-észlelés pásztázás nélkül. A Generation egy UInt64, amely 1-nél indul, és átforduláskor kihagyja a 0-t, így a 0 sosem olyan érték, amelyet a mag kibocsát, és megbízható sosem megfigyelt őrjelként működik. Két invariáns teszi használhatóvá: a generáció nem mozoghat, amíg bármilyen olvasási bérlemény létezik, és minden sikeres írástranzakció pontosan egyszer növeli. Így az IXLSWorkbookReadLease.Generation pillanatkép, amely a bérlemény teljes élete alatt konstans marad, az IXLSWorkbookWriteGuard.StartGeneration pedig megmondja az írónak, milyen volt a modell, amikor a tranzakciója kinyílt. Egy rács, egy nyomtatási előnézet vagy egy levezetett index egyetlen egészet hasonlíthat össze sorok diffelése helyett
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// A FCachedGeneration 0-nál indul, olyan értéknél, amelyet a mag sosem bocsát ki,
// így az első járás mindig újraépít
end;
Amit ez a koordináció nem ígér
Három korlátot érdemes pimaszul kimondani, mert a másik feltételezése az, ahogy a mechanizmus félhasználásra kerül. Először is az íróvédő nem kölcsönös kizárás az írók között: a mag az olvasókat zárja ki az írókkal szemben, és két különböző szál egyszerre is tarthat íróvédőt, mindegyik függetlenül léptetve a generációt — egy regressziós teszt pontosan ezt a viselkedést állítja. A saját írószálai sorbaállítása továbbra is az Ön dolga. Másodszor, itt semmi sem fájlockolás vagy folyamatok közti mutex; egy folyamaton belüli szálakat koordinál egyetlen munkafüzet-példányra, és két ugyanazt az .xlsx-et megnyitó folyamat semmit sem tud egymásról. Harmadszor, a garancia csak azokat a hívókat éri el, akik ténylegesen vesznek bérleményt — egy bérlemény nélküli olvasás továbbra is zárolatlan forró útvonalon jár, amely gyors és teljesen védtelen. Ez koordinációs mag, nem tranzakciós adatbázis
E határokon belül használva kis, őszinte primitív: kilenc dedikált regressziós teszt fedi le a több olvasót, mindkét konfliktusirányt, az újrabelépést, a sorrenden kívüli kiadást, a megszakított tranzakciókat és a szálak közti olvasás/írás és írás/írás versenyt, egy 1 328 tesztből álló, Win32-n és Win64-n átmenő készleten belül. Párosítsa a összeomlásbiztos, fokozatos átmeneti fájl mentési útvonallal, és egy háttér export olyanná válik, amelyen végig lehet gondolni — konzisztens, amíg olvas, atomi, amikor ír. Az olvasási bérlemények, íróvédők és a generációs számláló a klasszikus és csomagmotorok részeként szállnak a Delphi és C++Builder számára készült HotXLS Delphi Component-ban, engedélyezésükhöz konfiguráció nem kell