Taustasäie oli viemässä 40 000 rivin raporttia, kun UI-säie asetti yhden solun, ja levylle päätynyt tiedosto ei vastannut yhtään koskaan olemassa olleesta työkirjasta. HotXLS käsittelee kyseisen virheluokan kohteessa lxWorkbookView.pas, jossa IXLSWorkbookViewCore myöntää O(1)-lukija-leaset ja fail-fast-kirjoitusvartijat: kun lease on auki, jokainen mutaation sisääntulopiste nostaa poikkeuksen kirjoittamisen sijaan
Epäonnistuminen, joka saapuu ilman pinon jäljitystä
Työkirjan lukeminen ei ole koskaan yksi atominen operaatio. Raportin läpikäynti on kymmeniä tuhansia yksittäisiä solulukuja sekuntien yli levitettynä, ja yksi niiden väliin osuva SetValue riittää muuttamaan, mitä läpikäynnin loppu näkee. Klassinen moottori tekee tästä konkreettisen: TXLSCellRef.SetValue voi kutsua kohteen FSST.Remove pudottaakseen jaetun merkkijonon merkinnän, nollata kohteen FValueType ja mitätöidä kaavavälimuistin tilan, kaikki sillä välin, kun toinen säie on keskellä viittaisi juuri niihin rakenteisiin. Mikään ei kaadu paikallaan. Saat raportin, jonka välisummat eivät täsmää, tai viennin, joka lukee hiljaisesti merkkijonoindeksin, joka osoittaa nyt jonnekin muualle
HotXLS ei tarkoituksella ratkaise tätä kirjoittajia odottuttamalla. Lukija voi pitää työkirjaa useita sekunteja, ja VCL-sovelluksessa kirjoittaja on usein UI-callback tai tapahtumakäsittelijä pääsäikeessä — kyseisen säikeen tukahduttaminen taustavientiä valmiiksi odottamaan on huonompi lopputulos kuin muokkauksen epäonnistaminen. Joten koordinaatioydin nostaa kohteen EXLSWorkbookWriteGuardUnavailable sillä hetkellä, kun kirjoitusta yritetään avoimen leasen vastaan, ennen kuin yhtään kenttää on koskettu, ja kutsuja päättää, jonottako muokkaus, yrittääkö uudelleen vai kertoako käyttäjälle. Fail-fast-ristiriitoja, ei jonotettuja
Onko työkirja turvallista lukea kahdelta säikeeltä?
Kyllä, edellyttäen että molemmat lukijat pitävät leasea eikä kukaan kirjoita. IXLSWorkbookViewCore.AcquireReadLease ottaa kohteen TCriticalSection, kasvattaa laskuria, ottaa tilannekuvan nykyisestä sukupolvesta ja palauttaa kohteen IXLSWorkbookReadLease — vakioaika riippumatta siitä, pitääkö työkirja tuhatta vai miljoonaa solua. Mikä tahansa määrä leaseja elää rinnakkain, ne voidaan vapauttaa missä tahansa järjestyksessä, ja kukin pitää ytimen hengissä oman rajapintaviittauksensa kautta, joten lease, joka elää sen luoneen olion yli, on turvallinen eikä roikkuva osoitin. Molemmat moottorit osallistuvat: TXLSWorkbook kohteessa lxHandle.pas ja TXLSXWorkbook kohteessa lxHandleX.pas rakentavat kumpikin ytimen konstruktoriaan ja tarjoavat kohteet _AcquireReadLease ja _AcquireWriteGuard
Yhtä tärkeää on se, mitä lease ei lisää lukupolkuun. Kriittinen osio kattaa leasen hankinnan, leasen vapautuksen ja kirjoitustransaktion rajat — ei muuta. Tavallinen solukohtainen luku ei koskaan joudu lukon, monitorin tai atomin laskurin piiriin, joten leaseen tarttuminen maksaa yhden hankinnan ja yhden vapautuksen koko skannaukselle, ei yhtä per solu. Se on sama suunnitteluvietti kuin rinnakkaisen XLSX-jäsennyksen ja muistiallokaattorityön takana: maksa koordinaatiosta rajalla, ei koskaan sisimmässä silmukassa. Symmetrinen sääntö pätee myös — AcquireReadLease nostaa kohteen EXLSWorkbookReadLeaseUnavailable aina, kun WriteDepth on nollasta poikkeava, joten et voi avata leasea kirjoitustransaktion sisältä, ei edes kirjoittavalla säikeellä
uses
lxHandle, lxWorkbookView;
procedure TReportThread.Execute;
var
Lease: IXLSWorkbookReadLease;
Sheet: TXLSWorksheet;
Row: Integer;
Total: Double;
begin
// Nostaa kohteen EXLSWorkbookReadLeaseUnavailable, jos kirjoitus on käynnissä
Lease := FWorkbook._AcquireReadLease;
Sheet := FWorkbook.Sheets[1];
Total := 0;
for Row := 1 to 50000 do
Total := Total + Sheet.Cells[Row, 3].Value;
FTotal := Total;
// Lease poistuu näkyvyysalueesta tässä: sen viitemäärä putoaa nollaan,
// ReleaseReadLease ajetaan, ja kirjoittajat tulevat taas mahdollisiksi
end;
Missä kirjoitusvartija oikeasti istuu?
Alimmaisessa muokattavassa kerroksessa, ei koskaan sen päällä olevassa mukavuus-API:ssa. _AcquireWriteGuard kutsutaan kohteen TXLSCellRef.SetValue sisältä itsestään, mikä tarkoittaa, että jokainen siihen suppeneva julkinen polku — Range.Value, työarkin tekstimäärittely, solu solulta kopiointi, liittäminen — portitetaan kerran sen sijaan, että jokainen kääre toistaisi tarkistuksen, jonka tuleva kääre unohtaa. Kattavuus on tarkoituksella leveä: 55 vartijahankintaa kohteessa lxHandle.pas ja 37 kohteessa lxHandleX.pas ytimen esitelleen erän aikaan
Portitettu pinta kattaa solujen arvot ja solujen muotoilun, kohteen TXLSWorkbook.Open, kopioinnin ja liittämisen, määritellyt nimet (Add, uudelleennimeäminen, RefersTo, Visible, IsMacro, Comment, Delete), työarkin metadataa kuten Name, Zoom, Visible, StandardHeight, FreezePanes, Protect ja Activate, sivun asetukset, sivunvaihdot ja kohteen Calculate. Sijoittelu on koko pointti: vartija hankitaan ennen kuin ensimmäistä kenttää kirjoitetaan, ei validoida jälkeenpäin ilmoituskoukulla, joten hylätty mutaatio jättää mallin tavut identtisinä. Regressiotestisarja todistaa täsmälleen tämän lukemalla uudelleen arkin nimen, zoomin, näkyvyyden, vakiokorkeuden, marginaalit, suunnan ja sivunvaihtojen määrät jokaisen hylätyn kutsun jälkeen. Latauspolut saavat saman kohtelun kerroksen alempana, jossa ZIP-lukupuomi koordinoi rinnakkaisen inflaten pakettimuodoille
procedure TXLSWorksheet.Activate;
var
WriteGuard: IXLSWorkbookWriteGuard;
begin
// Hankitaan ennen kuin ensimmäistä kenttää kosketetaan, ei koskaan jälkeen
WriteGuard := FWorkbook._AcquireWriteGuard;
if not FSelected then
begin
FWorkbook.FWorkSheets.Deselect;
FSelected := True;
end;
FWorkbook.FWorkSheets.FActiveSheet := Self;
// Vain valmistunut uloin vartija etenee sukupolvea
WriteGuard.Complete;
end;
Miksi sisäkkäinen kirjoitus etenee sukupolvea vain kerran?
Koska kirjoitustransaktion määrittelee säikeen uloin vartija, ei kukin vartija erikseen. Ydin pitää säikeittäistä kirjoittajatilaa, joka sisältää säietunnisteen, syvyyden ja valmistumislipun. Toinen AcquireWriteGuard samalla säikeellä löytää kyseisen tilan ja kasvattaa kohteen Depth arvoa uuden transaktion luomisen sijaan, ja vasta kun Depth palaa nollaan — uloimman vartijan ollessa merkitty kohteeksi Complete — kohteen FGeneration arvo etenee. Tämä mahdollistaa sen, että korkean tason operaatio kuten Calculate tai Open kutsuu kymmentä vartioitua primitiiviä allaan ja rekisteröityy silti yhtenä muutoksena. Sisäiset Complete-kutsut tallennetaan, mutta ne eivät siirrä laskuria itsestään, ja vartijat voidaan vapauttaa järjestyksestä poikkevasti rikkomatta kirjanpitoa
Epäonnistumissuunta on yhtä lailla eksplisiittinen. Jos vartija vapautetaan ilman kohtetta Complete — tavallinen seuraus poikkeuksesta, joka purkaa rajapintaviittauksen — sukupolvi ei etene, koska kirjoitustransaktio ei koskaan väittänyt onnistuneensa. Katso selvästi, mitä se tarkoittaa: HotXLS ei rullaa osittaista muokkausta takaisin. Laskuri kirjaa, ettei yhtään onnistunutta transaktiota valmistunut, mikä on täsmälleen se signaali, jonka välimuisti tarvitsee, mutta mallin palauttaminen aiempaan tilaansa ei ole jotain, mitä viitemäärätty vartija voi tehdä puolestasi. Jos transaktion keskellä tapahtuva epäonnistuminen voi jättää työkirjan muotoon, jota et voi toimittaa, säilytä lähdetiedosto ja avaa se uudelleen, sen sijaan että luotat muistissa olevaan olioon
Mitä sukupolvelaskuri ostaa sinulle
Halpa vanhenemisen tunnistus ilman skannausta. Generation on UInt64, joka alkaa arvosta 1 ja ohittaa arvon 0 kierroksella, joten 0 ei ole koskaan arvo, jonka ydin myöntää, ja toimii luotettavana ei koskaan havaittu -vartijana. Kaksi muuttumattomuussääntöä tekevät siitä käyttökelpoisen: sukupolvi ei voi liikkua, kun mikään lukija-lease on olemassa, ja jokainen onnistunut kirjoitustransaktio kasvattaa sitä täsmälleen kerran. Joten IXLSWorkbookReadLease.Generation on tilannekuva, joka pysyy muuttumattomana leasen koko eliniän ajan, ja IXLSWorkbookWriteGuard.StartGeneration kertoo kirjoittajalle, miltä malli näytti, kun sen transaktio avautui. Ruudukko, tulostusesikatselu tai johdettu indeksi voi verrata yhtä kokonaislukua rivien erottelun sijaan
var
Lease: IXLSWorkbookReadLease;
begin
Lease := FWorkbook._AcquireReadLease;
if Lease.Generation <> FCachedGeneration then
begin
FCachedGeneration := Lease.Generation;
RebuildRowHeightCache;
end;
PaintVisibleRows;
// FCachedGeneration alkaa arvosta 0, jota ydin ei koskaan myönnä,
// joten ensimmäinenkin kierros rakentaa aina uudelleen
end;
Mitä tämä koordinaatio ei lupaa
Kolme rajaa kannattaa sanoa suoraan, koska toisin olettaminen on tapa, jolla mekanismi väärinkäytetään. Ensinnäkin kirjoitusvartija ei ole keskinäinen poissulkeminen kirjoittajien välillä: ydin sulkee lukijat pois kirjoittajilta, ja kaksi eri säiettä voivat kumpikin pitää kirjoitusvartijaa samaan aikaan, kumpikin etenevät sukupolvea itsenäisesti — regressiotesti todistaa täsmälleen tämän käyttäytymisen. Omien kirjoittajasäikeiden sarjallistaminen on yhä sinun tehtäväsi. Toiseksi, mikään tässä ei ole tiedostolukko eikä prosessien välinen mutex; se koordinoi säikeitä yhden prosessin sisällä yhtä työkirjainstanssia vastaan, ja kaksi prosessia, jotka avaavat saman .xlsx-tiedoston, eivät tiedä toisistaan mitään. Kolmanneksi, takuu yltää vain kutsujiin, jotka oikeasti ottavat leasen — leaseeton luku kulkee yhä lukitsematonta kuumaa polkua, joka on nopea ja täysin suojaamaton. Tämä on koordinaatioydin, ei transaktiotietokanta
Näiden rajojen sisällä käytettynä se on pieni, rehellinen primitiivi: yhdeksän erillistä regressiotestiä kattavat useita lukijoita, molemmat ristiriitasuunnat, uudelleensisääntyvyyden, järjestyksestä poikkeavan vapautuksen, keskeytetyt transaktiot sekä säikeiden väliset luku/kirjoitus- ja kirjoitus/kirjoitus-kilpailut 1 328 testin sarjan sisällä, joka läpäisee Win32:lla ja Win64:llä. Yhdistä se kaatumisturvalliseen vaiheistettuun väliaikaistiedostotallennukseen ja taustavienti muuttuu joksikin, jonka voit käsittää päästä päähän — johdonmukainen lukiessaan, atominen kirjoittaessaan. Lukija-leaset, kirjoitusvartijat ja sukupolvelaskuri toimituvat osana klassista ja pakettimoottoria tuotteessa HotXLS Delphi Component Delphille ja C++Builderille, eikä niiden käyttöönotto vaadi konfiguraatiota