Techninis straipsnis

HotXLS skaitymo nuomos ir rašymo sargai Delphi knygoms

Foninė gija eksportavo 40 000 eilučių ataskaitą, kai UI gija nustatė vieną langelį, ir failas, nukritęs ant disko, neatitiko nė vienos kada nors egzistavusios darbo knygos. HotXLS apdoroja tą klaidų klasę lxWorkbookView.pas, kur IXLSWorkbookViewCore išduoda O(1) skaitymo nuomas ir fail-fast rašymo sargus: kol nuoma atidaryta, kiekvienas mutacijos įėjimo taškas kelia išimtį vietoj rašymo

Gedimas, atkeliaujantis be stack trace

Darbo knygos skaitymas niekada nėra viena atominė operacija. Ataskaitos apėjimas yra dešimtys tūkstančių atskirų langelių skaitymų, išdėstytų per sekundes, ir vienas SetValue, nukritęs tarp dviejų jų, pakanka, kad pakeistų tai, ką likęs apėjimas mato. Klasikinis variklis tai padaro konkrečiu: TXLSCellRef.SetValue gali kviesti FSST.Remove, kad pašalintų shared string įrašą, atstatyti FValueType ir negalioti formulės talpyklos būseną — visa tai, kol kita gija yra pusiaukelėje dereferencuodama būtent tas struktūras. Niekas neužgriūva vietoje. Gaunate ataskaitą, kurios tarpinės sumos nesideda, arba eksportą, kuris tyliai skaito eilutės indeksą, dabar rodantį kitur

HotXLS tyčia nesprendžia to, priversdamas rašytojus laukti. Skaitytojas gali laikyti darbo knygą keletą sekundžių, o VCL programoje rašytojas dažnai yra UI callback arba įvykių handleris pagrindinėje gijoje — tos gijos užblokavimas, kol foninis eksportas pasibaigs, yra blogesnė išvada nei nepavykęs redagavimas. Tad koordinacijos branduolys kelia EXLSWorkbookWriteGuardUnavailable akimirka, kai rašymas bandomas prieš atidarytą nuomą, prieš paliečiant vienintelį lauką, o kvietėjas nusprendžia, ar eilinti redagavimą, kartoti, ar pasakyti naudotojui. Fail-fast konfliktai, ne eiliniai

HotXLS koordinacijos matrica, rodanti, kad skaitymo nuomos laisvai sugyvena, kad rašymas, bandomas prieš atidarytą nuomą, kelia EXLSWorkbookWriteGuardUnavailable, kad nuoma, prašoma rašymo transakcijos viduje, kelia EXLSWorkbookReadLeaseUnavailable, ir kad dvi rašytojų gijos niekada nesiatrenkia viena kitos
Skaitytojai sugyvena, o rašytojai prieš juos žlugsta greitai, bet branduolys niekada nesiatrenkia vienos rašytojų gijos nuo kitos

Ar darbo knygą saugu skaityti iš dviejų gijų?

Taip, jei abu skaitytojai laiko nuomą ir niekas nerašo. IXLSWorkbookViewCore.AcquireReadLease paima TCriticalSection, padidina skaitiklį, užfiksuoja dabartinę kartą ir grąžina IXLSWorkbookReadLease — pastovus laikas, nesvarbu, ar darbo knyga turi tūkstantį langelių, ar milijoną. Bet kiek nuomų sugyvena, jos gali būti išleidžiamos bet kuria tvarka, ir kiekviena prismeigia branduolį gyvą per savą sąsajos nuorodą, tad nuoma, išgyvenanti objektą, jį sukūrusį, yra saugi, o ne kabančioji rodyklė. Abu varikliai dalyvauja: TXLSWorkbook lxHandle.pas ir TXLSXWorkbook lxHandleX.pas kiekvienas sukonstruoja branduolį savo konstruktoriuje ir atskleidžia _AcquireReadLease bei _AcquireWriteGuard

Kas svarbu ne mažiau, tai ko nuoma neprideda prie skaitymo kelio. Kritinė sekcija dengia nuomos įgijimą, nuomos išleidimą ir rašymo transakcijų ribas — nieko kito. Paprastas per langelį skaitymas niekada neįeina į užraktą, monitorių ar atominį skaitiklį, tad nuomos laikymas kainuoja vieną įgijimą ir vieną išleidimą visam skenavimui, o ne po vieną per langelį. Tai tas pats dizaino instinktas už lygiagretaus XLSX analizavimo ir atminties allocatorio darbo: mokėti už koordinaciją riboje, niekada vidiniame cikle. Simetriška taisyklė taip pat galioja — AcquireReadLease kelia EXLSWorkbookReadLeaseUnavailable bet kada, kai WriteDepth nenulinis, tad nuomos negalite atidaryti rašymo transakcijos viduje, net rašančioje gijoje

HotXLS moka už koordinaciją skenavimo riboje: kritinė sekcija dengia tik nuomos įgijimą, išleidimą ir rašymo transakcijų ribas, o rašymo sargas įgyjamas TXLSCellRef.SetValue viduje, tad kiekviena patogi API virš jo užtveriama kartą
Vienas įgijimas ir vienas išleidimas dengia penkiasdešimt tūkstančių langelių skenavimą, ir vienas sargas TXLSCellRef.SetValue viduje dengia kiekvieną viešą rašymo kelią virš jo
uses
  lxHandle, lxWorkbookView;

procedure TReportThread.Execute;
var
  Lease: IXLSWorkbookReadLease;
  Sheet: TXLSWorksheet;
  Row: Integer;
  Total: Double;
begin
  // Kelia EXLSWorkbookReadLeaseUnavailable, jei rašymas vyksta
  Lease := FWorkbook._AcquireReadLease;
  Sheet := FWorkbook.Sheets[1];
  Total := 0;
  for Row := 1 to 50000 do
    Total := Total + Sheet.Cells[Row, 3].Value;
  FTotal := Total;
  // Nuoma palieka scope čia: jos nuorodų skaitiklis nukrenta iki nulio,
  // ReleaseReadLease suveikia, ir rašytojai vėl tampa įmanomi
end;

Kur rašymo sargas iš tikrųjų sėdi?

Žemiausiame kintamame sluoksnyje, niekada ant jo virš esančioje patogioje API. _AcquireWriteGuard kviečiamas iš paties TXLSCellRef.SetValue viduje, o tai reiškia, kad kiekvienas viešas kelias, susiaurėjantis į jį — Range.Value, darbalapio teksto priskyrimas, langelis po langelio kopijavimas, įklijavimas — užtveriamas kartą, vietoj to, kad kiekvienas wrapperis kartotų patikrą, kurios būsimas wrapperis užmirš. Danga tyčia plati: 55 sargų įgijimai lxHandle.pas ir 37 lxHandleX.pas toje partijoje, kuri pristatė branduolį

Užtvertas paviršius apima langelių reikšmes ir langelių formatavimą, TXLSWorkbook.Open, kopijavimą ir įklijavimą, apibrėžtuosius vardus (Add, pervadinimą, RefersTo, Visible, IsMacro, Comment, Delete), darbalapio metaduomenis, tokius kaip Name, Zoom, Visible, StandardHeight, FreezePanes, Protect ir Activate, puslapio sąrangą, puslapio lūžius ir Calculate. Išdėstymas yra visa esmė: sargas įgyjamas prieš parašant pirmąjį lauką, o ne patvirtinamas po to pranešimų kabliu, tad atmesta mutacija palieka modelį baitas po baito identišką. Regresijos rinkinys teigia būtent tai, perskaitydamas lapo vardą, zoom, matomumą, standartinį aukštį, paraštes, orientaciją ir puslapio lūžių skaičius po kiekvieno atmesto iškvietimo. Įkėlimo keliai gauna tą patį apdorojimą sluoksniu žemiau, kur ZIP skaitymo vartai koordinuoja lygiagretų inflate paketo formatams

procedure TXLSWorksheet.Activate;
var
  WriteGuard: IXLSWorkbookWriteGuard;
begin
  // Įgyjamas prieš paliečiant pirmąjį lauką, niekada po to
  WriteGuard := FWorkbook._AcquireWriteGuard;
  if not FSelected then
  begin
    FWorkbook.FWorkSheets.Deselect;
    FSelected := True;
  end;
  FWorkbook.FWorkSheets.FActiveSheet := Self;
  // Tik užbaigtas išorinis sargas pajudina kartą
  WriteGuard.Complete;
end;

Kodėl įdėtas rašymas pajudina kartą tik vieną kartą?

Nes rašymo transakciją apibrėžia išorinis gijos sargas, o ne kiekvienas sargas atskirai. Branduolys laiko per giją skirtą rašytojo būseną, laikančią gijos id, gylį ir užbaigimo flagą. Antras AcquireWriteGuard toje pačioje gijoje randa tą būseną ir padidina Depth, vietoj naujos transakcijos kūrimo, ir tik kai Depth sugrįžta iki nulio — su išoriniu sargu, pažymėtu CompleteFGeneration žengia. Būtent todėl aukšto lygio operacija, tokia kaip Calculate arba Open, gali kviesti dešimt užtvertų primitivų žemiau ir vis tiek užsiregistruoti kaip vienas pokytis. Vidiniai Complete iškvietimai užrašomi, bet patys neperstumia skaitiklio, o sargai gali būti išleidžiami ne eilės tvarka, nesugadinant apskaitos

Gedimo kryptis vienodai aiški. Jei sargas išleidžiamas be Complete — paprasta išimties, atvyniojančios sąsajos nuorodą, pasekmė — kartas nežengia, nes rašymo transakcija niekada nepareiškė sėkmės. Žiūrėkite aiškiai, ką tai reiškia: HotXLS neatlieka dalinio redagavimo atšaukimo. Skaitiklis užrašo, jog jokia sėkminga transakcija nesibaigė — būtent tas signalas, kurio reikia talpyklai, — bet modelio atstatymas į ankstesnę būseną nėra tai, ką nuorodų skaičiumi skaičiuojamas sargas gali padaryti už jus. Jei per transakciją įvykusi nesėkmė gali palikti darbo knygą pavidalu, kurio negalite išsiųsti, laikykite šaltinio failą ir atidarykite jį iš naujo, vietoj pasitikėjimo atminties objektu

Dvi HotXLS rašymo transakcijų laiko linijos palygintos: įdėti sargai vienoje gijoje pakelia gylį ir pajudina kartos skaitiklį tik kai išorinis sargas užbaigia, o išimtis, atvyniojanti sargus be Complete, palieka kartą nepakitusią ir dalinį redagavimą vietoje
Gylis seka įdėjimą, bet tik užbaigta išorinė transakcija pajudina kartą, ir nutrauktoji palieka ir skaitiklį, ir dalinį redagavimą tiksliai ten, kur jie buvo

Ką kartos skaitiklis jums nuperka

Pigus pasenimo aptikimas be skenavimo. Generation yra UInt64, prasidedantis nuo 1 ir praleidžiantis 0 apvyniojime, tad 0 niekada nėra reikšmė, kurią branduolys išduoda, ir veikia kaip patikimas „niekada nebūta stebėta“ sargas. Dvi invariantos padaro jį naudingą: kartas negali judėti, kol egzistuoja bet kuri skaitymo nuoma, ir kiekviena sėkminga rašymo transakcija padidina jį tiksliai kartą. Tad IXLSWorkbookReadLease.Generation yra momentinė kopija, liekanti pastovi visą nuomos gyvavimą, o IXLSWorkbookWriteGuard.StartGeneration pasako rašytojui, kaip modelis atrodė, kai jo transakcija atsidarė. Tinklelis, spausdinimo peržiūra arba išvestinis indeksas gali palyginti vieną sveikąjį vietoj eilučių diffinimo

var
  Lease: IXLSWorkbookReadLease;
begin
  Lease := FWorkbook._AcquireReadLease;
  if Lease.Generation <> FCachedGeneration then
  begin
    FCachedGeneration := Lease.Generation;
    RebuildRowHeightCache;
  end;
  PaintVisibleRows;
  // FCachedGeneration prasideda nuo 0, reikšmės, kurią branduolys
  // niekada neišduoda, tad pats pirmasis perėjimas visada persistato
end;

Ko ši koordinacija nežada

Trys ribos vertos aiškaus pasakymo, nes prielaida, kad kitaip, yra būdas, kuriuo mechanizmas suvartojamas neteisingai. Pirma, rašymo sargas nėra abipusis rašytojų išskyrimas: branduolys atskiria skaitytojus nuo rašytojų, ir dvi skirtingos gijos gali kiekviena laikyti rašymo sargą tuo pačiu metu, kiekviena pajudindama kartą nepriklausomai — regresijos testas teigia būtent tą elgesį. Savo rašytojų gijų serijizavimas vis tiek jūsų darbas. Antra, niekas čia nėra failo užraktas arba tarpprocesinis mutexas; jis koordinuoja gijas vieno proceso viduje prieš vieną darbo knygos instanciją, ir du procesai, atidarantys tą patį .xlsx, nieko vienas apie kitą nežino. Trečia, garantija pasiekia tik tuos kvietėjus, kurie iš tikrųjų pasiima nuomą — be nuomos skaitymas vis tiek eina neužrakintu karštu keliu, kuris greitas ir visiškai neapsaugotas. Tai koordinacijos branduolys, ne transakcinė duomenų bazė

Naudojamas tų ribų viduje tai mažas, sąžiningas primitivas: devyni specialūs regresijos testai dengia kelis skaitytojus, abi konfliktų kryptis, pakartotinumą, išleidimą ne eilės tvarka, nutrauktas transakcijas ir tarp gijų skaitymo/rašymo bei rašymo/rašymo lenktynes, rinkinyje iš 1,328 testų, einančių ant Win32 ir Win64. Suporuokite jį su atsargiu nuo užstrigimo pakopiniu laikino failo išsaugojimo keliu, ir foninis eksportas tampa dalyku, apie kurį galite samprototi nuo galo iki galo — nuoseklus, kol skaito, atominis, kai rašo. Skaitymo nuomos, rašymo sargai ir kartos skaitiklis atkeliavo kaip klasikinio ir paketo variklių dalis HotXLS Delphi Component Delphi ir C++Builder, be jokios konfigūracijos, reikalingos juos įjungti