PDFium ni varen za nite na ravni modula, zato si lahko dve instanci TPdf, ki delata na dveh različnih datotekah v dveh nitih, vseeno pokvarita druga drugo. Komponenta PDFium Component za Delphi to opravi na dva načina: od v3.125.1 ValidatePdfFilesParallel vrsti vsak izvorni klic PDFium za enim zaklepom po celem procesu, TPdf.RenderPagesParallel pa vsakemu delavcu da lastno izolirano kopijo modula PDFium. Hrošč, ki je vsilil popravek, je bila najhujša vrsta občasnega. Preizkus paketne validacije je večino časa šel skozi, potem pa je eno od dveh dobrich datotek poročal kot spodletelo, nato pa sesul naslednji preizkus v istem procesu s kršitvijo dostopa, včasih pa vzel s seboj celego poganjalnika z izstopno kodo namesto sledi skladu. Preizkus ni imel nič narobe in noben posamezen dokument ni imel nič narobe. Narobe je bila domneva: ena TPdf na nič ni izolacija
Zakaj ena TPdf na nit ne zadošča?
Ena TPdf na nit ne zadošča, ker PDFium hrani svoje nevarno stanje v modulu, ne v dokumentu. Vsaka TPdf lasti svoj ročaj FPDF_DOCUMENT, vsakemu ročaju v procesu pa služi isti naloženi DLL, ta DLL pa hrani singleton-e po celem procesu: predpomnilnik pisav, stranski modul in druge globalne strukture, ki se jih dotikajo nalaganje dokumentov, razčlenjevanje in izrisovanje. Dve niti, ki nalagata dve nepovezani datoteki, sta dve niti, ki istočasno pišeta v isti predpomnilnik pisav. Nihče na strani Delphi ne lasti teh podatkov, zato jih na strani Delphi nič ne more zakleniti na dokument
Komponenta sicer ima zaklep in iz njega je lahko sklepati napačno. TPdf ovije svoje izrisovalne poti v notranji kritični odsek (EnterRenderLock / LeaveRenderLock, privatni metodi TPdf). Ta zaklep je na instanco. Prepreči dvema nitema, da istega TPdf poganjata hkrati, kar je prava nevarnost, druge instance na drugi niti pa ne vidi, zato sočasnost med instancami gre naravnost mimo nje. Splošno pravilo je preprosto dovolj, da ga zapišemo v eni vrstici: v enem naloženem modulu PDFium sme biti v vsakem trenutku znotraj PDFiuma največ ena nič, ne glede na to, koliko dokumentov je odprtih
Kako izgleda pokvarjenost med dokumenti v procesu Delphi?
Pokvarjenost med dokumenti izgleda kot naključna mešanica nepovezanih spodletitev, škoda pa preživi kodo, ki jo je povzročila. Pred v3.125.1 je ValidatePdfFilesParallel ustvaril eno TPdf na delavsko nič in pognal Active := True plus gradnjo predpoletnega poročila sočasno na skupnem modulu. Simptomi, opaženi na gradnjah Delphi in Free Pascal, so pokrili celoten razpon:
- Veljavna datoteka se ne naloži ali pa pride iz paketa kot spodletela, čeprav bi morala iti skozi
- Kršitev dostopa pride na površje v poznejšem, nepovezanem klicu, pogosto v drugem preizkusu ali drugem dokumentu
- V Delphiju se pokaže
External exception C000001D. Ta koda jeSTATUS_ILLEGAL_INSTRUCTION, sprožena z inštrukcijoud2, ki jo izvajata makra internegaCHECKinIMMEDIATE_CRASHPDFium, kadar se zlomi neka invarianta - Proces izstopi s
0xC0000409(fail-fast, poročan kot prekoračitev medpomnilnika sklada) ali0xC0000374(pokvarjenost kupčke), brez vsakršne izjeme Delphi
Zadnji dve točki sta razlog, zakaj je bilo hrošča tako težko pripeti. Vzporedna validacija se je zaključila, pokvarjeno globalno stanje je ostalo za sabo in naslednja naprava v istem procesu se je o njega spotaknila. V enem regresijskem zagonu Delphi Win64 je val spodletitev C000001D zadel preizkuse, ki paketne validacije nikoli niso smeli; bili so preprosto prva koda, ki je po škodi uporabila PDFium. Izmerjene številke naredijo obseg jasen. Preskusni program Delphi, ki je enak vzorec pognal skozi dva delavca, je v enem zagonu spodletel na 122 od 160 dokumentov, v drugem na 138 od 160, eden od teh zagonov pa je naravnost sprožil External exception C000001D. Stresni primer 8 dokumentov, 4 delavcev in 5 krogov je spodletel ali sesul v 5 od 5 zagonov na Free Pascal Win64. Po popravku je isti preskus spodletel na 0 od 1200 dokumentov
Kako ValidatePdfFilesParallel ostane varen od v3.125.1
ValidatePdfFilesParallel zdaj vrsti izvorno polovico vsakega posla in upravljano polovico pusti vzporedno. Vsak delavec vzame eno kritični odsek na ravni enote, preden ustvari svojo TPdf, in ga drži skozi FileName, Active := True, gradnjo predpoletnega poročila in Free. Ustvarjanje in uničenje sta znotraj zaklepa namenoma: zapiranje dokumenta kliče nazaj v modul tako kot nalaganje. Ko delavec dobi zajet zapis TPdfPreflightReport, sprosti zaklep in pravila validacije vrednoti proti temu zapisu, kar se ne dotika nobenega stanja PDFium, zato vrednotenje pravil za eno datoteko prekriva delo PDFium za naslednjo
S popravkom sta prišli dve manjši spremembi. Spodletitev nalaganja zdaj sproži EPdfError z LastLoadReport.ErrorMessage, tako da ErrorMessage elementa poimenuje dejansko težavo razčlenjevanja namesto sekundarne napake »ni aktivnega dokumenta«. In cena je iskreno navedena: PDFium del paketa je zdaj serijski, zato pri paketu, ki ga dominirata razčlenjevanje in preflight, dodatni delavci ne kupijo veliko. Če ste na različici pred v3.125.1, nastavite WorkerCount na 1; to odstrani sočasnost in z njo pokvarjenost
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 = število procesorjev, omejeno na 8
Options.Standards := [ppsPdfA];
// Z izrecnim registrom izberite ujemajoč profil sami.
// Prazna tabela Profiles požene vsako registrirano pravilo, pravila za
// standarde, ki jih niste preflightali, pa poročajo "did not pass"
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;
Podajanje nil kot registra je krajša pot: ValidatePdfFilesParallel potem sam ustvari privzeti register, seznam profilov izpelje iz Options.Standards in register sprosti, ko se vrne. Rezultati vedno pridejo nazaj v vrstnem redu vnosa, kakršen koli že je bil vrstni red, v katerem so delavci končali. Za formate poročil in ovijalnik ukazne vrstice okoli istega pogona glej paketa predpoletnih poročil PDF s CLI komponente PDFium Component, kaj sami preizkusi PDF/A pokrijejo pa validacija preflight PDF/A v Delphiju
Kako RenderPagesParallel poganja strani resnično vzporedno?
TPdf.RenderPagesParallel teče vzporedno, ker njegovi delavci nikoli ne delijo modula PDFium. Metoda najprej shrani aktivni dokument v izvorno shrambo na klicajoči niti. Vsak delavec nato kopira naloženi DLL PDFium v enolično poimenovano datoteko v začasni imenik, to kopijo naloži z LoadLibrary in jo inicializira. Windows DLL, naložen iz druge poti, obravnava kot drugačen modul, tako da vsaka kopija dobi svoje globalne podatke: svoj predpomnilnik pisav, svoj stranski modul, svoje vse. Delavec odpre shranjen dokument v svojem zasebnem modulu, postopoma izriše svoje strani s preizkusi preklica med koraki, nato uniči knjižnico, razloži kopijo in izbriše datoteko
Izolacija ni brezplačna in privzetosti to odražajo. Vsak delavec plača kopijo DLL na disku, drugi nabor globalnih podatkov PDFium v pomnilniku in svežo razčlenitev dokumenta. MaxWorkers = 0 pomeni največ 4 delavce, MaxPixelsPerPage in MaxTotalOutputBytes omejita surov izhod, obrnjeni in nočni dvo-tonski možnosti izrisa pa sta zavrnjeni, ker se medpomnilniki vračajo surovi. Rezultat je TPdfParallelRenderReport, katerega tabela Results drži en zgornje-navzdol 32-bitni medpomnilnik na zahtevano stran, v vrstnem redu zahtev
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; // številke strani so eno-osnovne
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// Izvorni posnetek se naredi na skupnem modulu, zato držite zaklep
// PDFium po celem procesu, kadar tudi druge nite uporabljajo 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;
Upoštevajte zaklep okoli klica. Delavski moduli so zasebni, korak posnetka na začetku pa požene SaveAs na skupnem modulu iz klicajoče niti. Če v vašem procesu nič drugega sočasno ne dotakne TPdf, lahko zaklep spustite; če karkoli ga dotakne, potrebuje posnetek isto zaščito kot vsak drug klic skupnega modula
| Vzorec | Varno med dokumenti | Delo PDFium teče vzporedno | Cena |
|---|---|---|---|
Ena TPdf na nit, brez skupnega zaklepa | Ne | Da, dokler ne pokvari | Občasna sesutja, poškodovano stanje procesa |
| En zaklep po celem procesu okoli vseh klicev PDFium | Da | Ne | PDFium del je serijski |
ValidatePdfFilesParallel od v3.125.1 | Da | Ne; vrednotenje pravil je vzporedno | Razčlenjevanje in preflight sta serijska |
TPdf.RenderPagesParallel | Da | Da | Kopija DLL, pomnilnik in sveža razčlenitev na delavca |
Kako naj uredite svojo večnitno kodo PDFium?
Vaše nite naj si delijo en zaklep po celem procesu in ga držijo za celotno življenje vsake TPdf, ki jo uporabljajo, ali pa uporabijo API komponente, ki modul izolira namesto njih. Zaklep mora biti en sam objekt za ves proces, ne eden na nit, na obrazec ali na dokument; zaklep, ki ga dve niti ne delita, ne ščiti nič. Vzorec spodaj zrcali, kar komponenta dela interno od v3.125.1: ustvari, naloži, preberi in sprosti znotraj zaklepa, vse, česar se ne dotika PDFium, pa zunaj njega
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // en zaklep za ves proces
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; // zapiranje dokumenta je tudi delo PDFium
end;
finally
PdfiumLock.Release;
end;
// Pod to vrstico ni PDFium, zato ta del teče vzporedno
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Nekaj pravil ohrani vzorec poštenega v pravi aplikaciji:
TPdf.CreateinFreedajte znotraj zaklepa, ne samo očitnih klicev. Nalaganje, zapiranje, branja lastnosti, kot jePageCount, spremembe strani, izločanje besedila, izrisovanje in shranjevanje vsa dosežejo v modul- Preverite
Activepo dodelitvi. Spodletel nalaganje pustiActivenaFalse,LastLoadReport.ErrorMessagepa pove zakaj - Zaklep držite na dokument, ne na klic. Finejše zaklepanje je v načelu mogoče, a samo, če noben član
TPdfnikoli ne steče izven njega, groba različica pa je tista, na katero se zanaša sama komponenta - Počasno delo brez PDFium, kot so zapisi v bazo, indeksiranje in klici po omrežju, držite izven zaklepa, sicer bo en počasen potrošnik vrstil vse v serijo
- Zasebnega zaklepa na instanco za izrisovanje ne obravnavajte kot nadomestek. Ščiti enega
TPdfpred samim seboj in nič več
Ista previdnost velja za kodo, ki je niste napisali kot surove niti. Ozadnje prihodnosti so dober način, da dolgi izrisi ostanejo izven niti vmesnika, kot opisuje izrisovanje PDF v ozadju s preklicljivimi prihodnostmi, izvrševalec prihodnosti pa ne doda lastnega globalnega zaklepa PDFium. Če lahko več prihodnosti istočasno poganja različne instance TPdf, vzemite isti zaklep po celem procesu znotraj vsakega delavca in pregledovalnik na glavni niti obravnavajte kot še enega klienta skupnega modula. Uporaba med instancami skozi asinhrone API-je ni bila posebej revidirana, zato je konservativna domneva, da potrebuje isto serizacijo kot ročno napisane niti. Ko potrebujete pravo vzporednost PDFium za kaj drugega kot izrisovanje strani, ločeni delavski procesi vsakemu poslu dajo lasten modul po konstrukciji
Hiter pregled: pravila nitenja PDFium za Delphi
- Nevarno stanje PDFium je po celem modulu: predpomnilnik pisav, stranski modul in drugi globalni podatki si jih delijo vsi dokumenti v procesu
- Ena
TPdfna nič ne izolira ničesar; dve instanci na dveh nitih si lahko še vedno pokvarita druga drugo - Tipični simptomi so spodletitve nalaganja, kršitve dostopa v poznejši kodi,
External exception C000001Din izhodi s0xC0000409ali0xC0000374 - Pokvarjenost vztraja v procesu, zato klic, ki spodleti, pogosto ni tisti, ki jo je povzročil
ValidatePdfFilesParallelje varen od v3.125.1; na starejših različicah uporabiteWorkerCount := 1TPdf.RenderPagesParallelje resnično vzporeden, ker vsak delavec naloži izolirano kopijo modula PDFium- Vaše lastne nite, naloge in prihodnosti potrebujejo en zaklep po celem procesu, ki pokrije vsako
TPdfodCreatedoFree
Komponenta PDFium Component ovije pogon PDFium za Delphi s paketnim preflightom in validacijo, izoliranim vzporednim izrisovanjem, preklicljivim ozadnjim delom in podrobnimi diagnostikami nalaganja. Podrobnosti in izdaje so na strani izdelka PDFium Component