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
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
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 Complete — FGeneration ž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
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