PDFium ei ole säievarainen moduulitasolla, joten kaksi TPdf-instanssia, jotka työskentelevät kahdella eri tiedostolla kahdessa säikeessä, voivat silti turmella toisiaan. PDFium Component Delphille hoitaa tämän kahdella tavalla: versiosta v3.125.1 alkaen ValidatePdfFilesParallel sarjoittaa jokaisen natiivin PDFium-kutsun yhden prosessinlaajuisen lukon taakse, kun taas TPdf.RenderPagesParallel antaa jokaiselle työntekijälle oman eristetyn kopionsa PDFium-moduulista. Korjauksen pakottanut vika oli pahin mahdollinen kertaluontoinen. Erävalidointitesti läpi useimmiten, sitten ilmoitti yhden kahdesta hyvästä tiedostosta epäonnistuneeksi, sitten kaatoi saman prosessin seuraavan testin pääsyvirheellä ja joskus vei koko ajon alas poistumiskoodilla pinon jäljityksen sijaan. Testissä ei ollut mitään vikaa, eikä missään yksittäisessä asiakirjassa ollut mitään vikaa. Oletus oli väärä: yksi TPdf säiettä kohden ei ole eristystä
Miksi yksi TPdf säiettä kohden ei riitä?
Yksi TPdf säiettä kohden ei riitä, koska PDFium pitää turvattoman tilansa moduulissa, ei asiakirjassa. Jokainen TPdf omistaa oman FPDF_DOCUMENT-kahvansa, mutta jokainen kahva prosessissa palvelee samaa ladattua DLL:ää, ja kyseinen DLL sisältää prosessinlaajuisia singletonrakenteita: fonttivälimuistin, sivumoduulin ja muita globaaleja rakenteita, joita asiakirjan lataaminen, jäsentäminen ja renderöinti kaikki koskettavat. Kaksi säiettä, jotka lataavat kaksi toisiinsa liittymätöntä tiedostoa, ovat kaksi säiettä, jotka kirjoittavat samaan fonttivälimuistiin samaan aikaan. Kukaan ei omista kyseistä dataa Delphi-puolella, joten mikään Delphi-puolella ei voi lukita sitä asiakirjaa kohden
Komponentilla on kyllä lukko, ja siitä on helppo vetää väärä johtopäätös. TPdf käärii omat renderöintipolunsa sisäiseen critical section -osioon (EnterRenderLock / LeaveRenderLock, TPdf:n yksityiset metodit). Kyseinen lukko on instanssia kohden. Se estää kahta säiettä ajamasta samaa TPdfia yhtä aikaa, mikä on aito vaara, mutta se ei näe toista instanssia toisella säikeellä, joten instanssien välinen samanaikaisuus kulkee suoraan sen ohi. Yleissääntö on yksinkertainen sanoa yhdellä rivillä: yhdessä ladatussa PDFium-moduulissa korkeintaan yksi säie saa olla PDFiumin sisällä milloin tahansa, riippumatta siitä, kuinka monta asiakirjaa on avoinna
Miltä asiakirjojen välinen turmeltuminen näyttää Delphi-prosessissa?
Asiakirjojen välinen turmeltuminen näyttää satunnaiselta sekoitukselta toisiinsa liittymättömiä vikoja, ja vahinko elää aiheuttaneen koodin ikänsä. Ennen versiota v3.125.1 ValidatePdfFilesParallel loi yhden TPdfin työntekijäsäiettä kohden ja ajoi kohteen Active := True plus preflight-raportin rakentamisen samanaikaisesti jaetulla moduulilla. Sekä Delphi- että Free Pascal -koontiversioilla nähdyt oireet kattoivat koko kirjon:
- Kelvollinen tiedosto epäonnistuu latautumaan tai palaa erästä epäonnistuneena, vaikka sen olisi pitänyt läpäistä
- Pääsyvirhe nousee pintaan myöhemmässä, toisiinsa liittymättömässä kutsussa, usein eri testissä tai eri asiakirjassa
External exception C000001Dilmestyy Delphissä. Kyseinen koodi onSTATUS_ILLEGAL_INSTRUCTION, jonka nostaaud2-käsky, jonka PDFiumin sisäisetCHECK- jaIMMEDIATE_CRASH-makrot suorittavat, kun invariantti rikkoutuu- Prosessi poistuu muodossa
0xC0000409(fail fast, ilmoitettuna pinopuskurin ylivuotona) tai0xC0000374(keon turmeltuminen), ilman mitään Delphi-poikkeusta
Kaksi viimeistä kohtaa ovat syy siihen, miksi vika oli niin vaikea paikallistaa. Rinnakkainen validointi päättyi, turmeltunut globaali tila jäi jäljelle, ja saman prosessin seuraava testitapaus kompastui siihen. Yhdessä Delphi Win64 -regressioajossa aalto C000001D-vikoja osui testeihin, jotka eivät koskaan koskeneet erävalidointiin; ne olivat yksinkertaisesti ensimmäinen koodi, joka käytti PDFiumia vahingon jälkeen. Mitatut luvut tekevät laajuudesta selvän. Delphi-koeajo, joka ajoi saman näytteen kahden työntekijän kautta, epäonnistui 122 dokumentilla 160:sta yhdessä ajossa ja 138:lla 160:sta toisessa, ja yksi kyseisistä ajoista nosti External exception C000001Din suoraan. Stressitapaus 8 dokumentilla, 4 työntekijällä ja 5 kierroksella epäonnistui tai kaatui 5:ssä 5 ajosta Free Pascal Win64:llä. Korjauksen jälkeen sama koeajo epäonnistui 0:lla 1 200 dokumentista
Miten ValidatePdfFilesParallel pysyy turvallisena versiosta v3.125.1 alkaen
ValidatePdfFilesParallel sarjoittaa nykyään jokaisen työn natiivin puoliskon ja pitää hallitun puoliskon rinnakkaisena. Jokainen työntekijä ottaa yhden yksikkötason critical section -osion ennen kuin se luo TPdfinsa ja pitää sitä kohtien FileName, Active := True, preflight-raportin rakentamisen ja Freein ajan. Luominen ja tuhoaminen ovat lukon sisällä tahallaan: asiakirjan sulkeminen kutsuu takaisin moduuliin yhtä lailla kuin lataaminen. Kun työntekijällä on siepattu TPdfPreflightReport-tietue, se vapauttaa lukon ja arvioi validointisäännöt kyseistä tietuetta vasten, mikä ei kosketa PDFium-tilaa, joten yhden tiedoston sääntöarviointi menee päällekkäin seuraavan PDFium-työn kanssa
Kaksi pienempää muutosta tuli korjauksen mukana. Latausvika nostaa nykyään EPdfErrorin kohteen LastLoadReport.ErrorMessage kanssa, joten tietueen ErrorMessage nimeää varsinaisen jäsennysongelman toissijaisen "ei aktiivista asiakirjaa" -virheen sijaan. Ja kustannus on sanottu rehellisesti: erän PDFium-osa on nykyään sarjallinen, joten jäsentämisen ja preflightin hallitsemalla erällä ylimääräiset työntekijät ostavat vähän. Jos olet versiolla ennen v3.125.1:tä, aseta WorkerCount arvoon 1; se poistaa samanaikaisuuden ja turmeltumisen mukanaan
uses
System.SysUtils, PDFium, FPdfPreflightReport;
procedure ValidateBatch(const Files: array of string);
var
Registry: TPdfValidationRuleRegistry;
Options: TPdfBatchValidationOptions;
Report: TPdfBatchValidationReport;
I: Integer;
begin
Registry := CreateDefaultPdfValidationRuleRegistry;
try
Options := TPdfBatchValidationOptions.Default;
Options.WorkerCount := 4; // 0 = suoritinmäärä, ylärajana 8
Options.Standards := [ppsPdfA];
// Eksplisiittisellä rekisterillä valitse vastaava profiili itse.
// Tyhjä Profiles-luettelo ajaa jokaisen rekisteröidyn säännön, ja säännöt
// standardeille, joita ei preflightattu, ilmoittavat "ei läpäissyt"
SetLength(Options.ValidationOptions.Profiles, 1);
Options.ValidationOptions.Profiles[0] := 'PDF/A';
Report := ValidatePdfFilesParallel(Files, Registry, Options);
finally
Registry.Free;
end;
for I := 0 to High(Report.Results) do
case Report.Results[I].Status of
pbvisPass: Writeln('PASS ', Report.Results[I].FileName);
pbvisFail: Writeln('FAIL ', Report.Results[I].FileName);
pbvisError: Writeln('ERROR ', Report.Results[I].FileName, ': ',
Report.Results[I].ErrorMessage);
else
Writeln('SKIP ', Report.Results[I].FileName); // pbvisCancelled
end;
Writeln(Report.PassedDocumentCount, ' passed, ',
Report.FailedDocumentCount, ' failed, ',
Report.ErrorDocumentCount, ' errors');
end;
nilin välittäminen rekisterinä on lyhyempi tie: ValidatePdfFilesParallel luo sitten oletusrekisterin itse, johtaa profiililuettelon kohteesta Options.Standards ja vapauttaa rekisterin palatessaan. Tulokset palaavat aina syötejärjestyksessä, riippumatta siitä, missä järjestyksessä työntekijät valmistuivat. Raporttimuodoista ja saman moottorin ympärillä olevasta komentorivikääreestä kertoo artikkeli erä-PDF-preflight-raportit PDFium Component CLI:llä, ja siitä, mitä PDF/A-tarkistukset itse kattavat, artikkeli PDF/A-preflight-validointi Delphissä
Miten RenderPagesParallel ajaa sivuja aidosti rinnakkain?
TPdf.RenderPagesParallel ajaa rinnakkain, koska sen työntekijät eivät koskaan jaa PDFium-moduulia. Metodi tallentaa ensin aktiivisen asiakirjan lähdetietovarastoon kutsuvalle säikeelle. Jokainen työntekijä kopioi sitten ladatun PDFium-DLL:n yksilöllisesti nimettyyn tiedostoon väliaikaiskansiossa, lataa kyseisen kopion funktiolla LoadLibrary ja alustaa sen. Windows käsittelee eri polusta ladattua DLL:ää eri moduulina, joten jokainen kopio saa omat globaalinsa: oman fonttivälimuistinsa, oman sivumoduulinsa, oman kaikkensa. Työntekijä avaa tallennetun asiakirjan yksityisessä moduulissaan, renderöi sivunsa edetessä perumistarkistusten kanssa vaiheiden välissä, tuhoaa sitten kirjaston, purkaa kopion ja poistaa tiedoston
Eristys ei ole ilmaista, ja oletukset heijastavat sitä. Jokainen työntekijä maksaa DLL-kopiosta levylle, toisesta joukosta PDFium-globaaleja muistissa ja tuoreesta asiakirjan jäsennyksestä. MaxWorkers = 0 tarkoittaa korkeintaan 4 työntekijää, kohteet MaxPixelsPerPage ja MaxTotalOutputBytes rajaavat raakatuotoksen, ja käänteiset sekä yöduotone-renderöintiasetukset torjutaan, koska puskurit palautetaan raakoina. Tulos on TPdfParallelRenderReport, jonka Results-taulukko kantaa yhden ylhäältä alas -32-bittisen puskurin pyydettyä sivua kohden, pyyntöjärjestyksessä
procedure RenderAllPages(Pdf: TPdf);
var
Options: TPdfParallelRenderOptions;
Report: TPdfParallelRenderReport;
Pages: array of Integer;
I: Integer;
begin
SetLength(Pages, Pdf.PageCount);
for I := 0 to High(Pages) do
Pages[I] := I + 1; // sivunumerot ovat ykköspohjaisia
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// Lähteen tilannekuva otetaan jaetulla moduulilla, joten pidä
// prosessinlaajuinen PDFium-lukko, jos muutkin säikeet käyttävät TPdf:ää
PdfiumLock.Acquire;
try
Report := Pdf.RenderPagesParallel(Pages, Options);
finally
PdfiumLock.Release;
end;
for I := 0 to High(Report.Results) do
if Report.Results[I].Status = pprsSucceeded then
SavePageBuffer(Report.Results[I]) // Width, Height, Stride, PixelFormat, Pixels
else
Writeln('Page ', Report.Results[I].PageNumber, ': ',
Report.Results[I].ErrorMessage);
end;
Huomaa lukko kutsun ympärillä. Työntekijöiden moduulit ovat yksityisiä, mutta alussa oleva tilannekuavaihe ajaa SaveAsin jaetulla moduulilla kutsuvalta säikeeltä. Jos mikään muu prosessissasi ei kosketa kohtetta TPdf samanaikaisesti, voit pudottaa lukon; jos jokin tekee niin, tilannekuva tarvitsee saman suojan kuin jokainen muu jaetun moduulin kutsu
| Kuvio | Turvallinen asiakirjojen yli | PDFium-työ ajaa rinnakkain | Kustannus |
|---|---|---|---|
Yksi TPdf säiettä kohden, ei jaettua lukkoa | Ei | Kyllä, kunnes se turmeltuu | Kertaluontoisia kaatumisia, vaurioitunut prosessin tila |
| Yksi prosessinlaajuinen lukko kaikkien PDFium-kutsujen ympärillä | Kyllä | Ei | PDFium-osa on sarjallinen |
ValidatePdfFilesParallel versiosta v3.125.1 alkaen | Kyllä | Ei; sääntöarviointi on rinnakkainen | Jäsentäminen ja preflight ovat sarjallisia |
TPdf.RenderPagesParallel | Kyllä | Kyllä | DLL-kopio, muisti ja tuore jäsentys työntekijää kohden |
Miten sinun kannattaa jäsentää oma monisäikeinen PDFium-koodisi?
Oman säikeesi pitäisi jakaa yksi prosessinlaajuinen lukko ja pitää sitä jokaisen käyttämänsä TPdfin koko eliniän ajan, tai muuten käyttää komponentin API:a, joka eristää moduulin puolestasi. Lukon on oltava yksi objekti koko prosessille, ei yksi säiettä, lomaketta tai asiakirjaa kohden; lukko, jota kaksi säiettä eivät jaa, ei suojele mitään. Alla oleva kuvio peilaa sitä, mitä komponentti tekee sisäisesti versiosta v3.125.1 alkaen: luo, lataa, lue ja vapauta lukon sisällä, tee sitten kaikki, mikä ei kosketa PDFiumia sen ulkopuolella
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // yksi lukko koko prosessille
type
TTextExtractThread = class(TThread)
private
FFileName: string;
FText: string;
protected
procedure Execute; override;
public
constructor Create(const AFileName: string);
property ExtractedText: string read FText;
end;
constructor TTextExtractThread.Create(const AFileName: string);
begin
inherited Create(True);
FFileName := AFileName;
end;
procedure TTextExtractThread.Execute;
var
Pdf: TPdf;
Page: Integer;
Raw: TStringBuilder;
begin
Raw := TStringBuilder.Create;
try
PdfiumLock.Acquire;
try
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FFileName;
Pdf.Active := True;
if not Pdf.Active then
raise EPdfError.Create(Pdf.LastLoadReport.ErrorMessage);
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Raw.AppendLine(Pdf.Text);
end;
finally
Pdf.Free; // asiakirjan sulkeminen on myös PDFium-työtä
end;
finally
PdfiumLock.Release;
end;
// Ei PDFiumia tämän rivin alapuolella, joten tämä osa ajaa rinnakkain
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Muutama sääntö pitää kuvion rehellisenä oikeassa sovelluksessa:
- Pane
TPdf.CreatejaFreelukon sisälle, ei vain ilmeiset kutsut. Lataaminen, sulkeminen, ominaisuusluvut kutenPageCount, sivunvaihdot, tekstin poiminta, renderöinti ja tallentaminen ylettävät kaikki moduuliin - Tarkista
Activesijoittamisen jälkeen. Epäonnistunut lataus jättää kohteenActivearvoonFalse, jaLastLoadReport.ErrorMessagekertoo miksi - Pidä lukko asiakirjaa kohden eikä kutsua kohden. Hienojakoisempi lukitus on mahdollinen periaatteessa, mutta vain, jos yksikään
TPdf-jäsen ei koskaan aja sen ulkopuolella, ja karkea versio on se, johon komponentti itse nojaa - Pidä hidas ei-PDFium-työ, kuten tietokantakirjoitukset, indeksointi ja verkkokutsut, lukon ulkopuolella, tai yksi hidas kuluttaja sarjoittaa kaiken
- Älä kohtele yksityistä instanssikohtaista renderöintilukkoa korvikkeena. Se vartioi yhtä
TPdfia itseään vastaan eikä mitään muuta
Sama varovaisuus koskee koodia, jota et kirjoittanut raakoina säikeinä. Taustafutuurit ovat hyvä tapa pitää pitkät renderöinnit poissa UI-säikeeltä, kuten artikkelissa taustalla tapahtuva PDF-renderöinti peruttavilla futuureilla kuvataan, mutta futuurien suorittaja ei lisää globaalia PDFium-lukkoa omasta puolestaan. Jos useat futuurit voivat ajaa eri TPdf-instansseja samaan aikaan, ota sama prosessinlaajuinen lukko kunkin työntekijän sisällä ja kohtele pääsäikeen katselinta yhtenä lisäasiakkaana jaetulle moduulille. Instanssien välistä käyttöä asynkronisten API:jen kautta ei ole auditoitu erikseen, joten konservatiivinen oletus on, että se tarvitsee saman sarjoituksen kuin käsin kirjoitetut säikeet. Kun tarvitset aitoa PDFium-rinnakkaisuutta johonkin muuhun kuin sivujen renderöintiin, erilliset työntekijäprosessit antavat jokaiselle työlle oman moduulinsa rakenteen kautta
Pikaopas: PDFiumin säikeityssäännöt Delphille
- PDFiumin turvaton tila on moduulinlaajuinen: fonttivälimuisti, sivumoduuli ja muut globaalit ovat jokaisen prosessin asiakirjan jaettuja
- Yksi
TPdfsäiettä kohden ei eristä mitään; kaksi instanssia kahdella säikeellä voivat silti turmella toisiaan - Tyypillisiä oireita ovat latausviat, pääsyvirheet myöhemmässä koodissa,
External exception C000001Dja poistumiset muodossa0xC0000409tai0xC0000374 - Turmeltuminen pysyy prosessissa, joten epäonnistuva kutsu on usein ei se, joka aiheutti sen
ValidatePdfFilesParallelon turvallinen versiosta v3.125.1 alkaen; vanhemmilla versioilla käytä arvoaWorkerCount := 1TPdf.RenderPagesParallelon aidosti rinnakkainen, koska jokainen työntekijä lataa eristetyn kopion PDFium-moduulista- Omat säikeesi, tehtäväsi ja futuurisi tarvitsevat yhden prosessinlaajuisen lukon, joka kattaa jokaisen
TPdfin kohteestaCreatekohteeseenFree
PDFium Component käärii PDFium-moottorin Delphille erä-preflightilla ja validoinnilla, eristetyllä rinnakkaisella renderöinnillä, peruttavalla taustatyöllä ja yksityiskohtaisilla latausdiagnostiikoilla. Yksityiskohdat ja versiot ovat PDFium Component -tuotesivulla