Műszaki cikk

HotXLS olvasási bérlemény és íróvédő Delphi munkafüzetekhez

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

HotXLS koordinációs mátrix, amely bemutatja, hogy az olvasási bérlemények szabadon együtt élnek, hogy egy nyitott bérleménnyel szemben kísérelt írás EXLSWorkbookWriteGuardUnavailable-t dob, hogy egy írástranzakción belül kért bérlemény EXLSWorkbookReadLeaseUnavailable-t dob, és hogy két írószál sosem zárja ki egymást
Az olvasók együtt élnek, és az írók velük szemben gyorsan hibáznak, de a mag sosem zár ki egy írószálat egy másikból

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

A HotXLS a pásztázás határán fizet a koordinációért: a kritikus szakasz csak a bérleménymegszerzést, kiadást és az írástranzakció határait fedi le, az íróvédő pedig a TXLSCellRef.SetValue belsejében szereződik, így minden fölötte lévő kényelmi API egyszer kapuztatott
Egy megszerzés és egy kiadás fed egy ötvenezer cellás pásztázást, és a TXLSCellRef.SetValue belsejében lévő egyetlen védő fed minden fölötte lévő nyilvános írási útvonalat
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

Két HotXLS írástranzakció idővonal összehasonlítva: egy szálon beágyazott védők a mélységet emelik, és csak akkor léptetik a generációs számlálót, amikor a legkülső védő befejeződik, miközben egy Complete nélkül visszatekercselő kivétel változatlanul hagyja a generációt és a részleges szerkesztést a helyén
A mélység a beágyazást követi, de csak egy befejezett legkülső tranzakció lépteti a generációt, és egy megszakított pontosan ott hagyja a számlálót és a részleges szerkesztést, ahol voltak

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