PDFium er ikke trådsikkert på modulnivå, så to TPdf-instanser som jobber på to ulike filer i to tråder, kan fortsatt korrumpere hverandre. PDFium Component for Delphi håndterer dette på to måter: siden v3.125.1 serialiserer ValidatePdfFilesParallel hvert native PDFium-kall bak én prosessomfattende lås, mens TPdf.RenderPagesParallel gir hver arbeider sin egen isolerte kopi av PDFium-modulen. Buggen som tvang frem fiksen, var den verste typen intermittent. En batchvalideringstest passerte mesteparten av tiden, rapporterte så én av to gode filer som feilet, krasjet så neste test i samme prosess med en access violation, og tok noen ganger hele testløperen ned med en exit-kode i stedet for en stack trace. Ingenting var galt med testen, og ingenting var galt med noe enkelt dokument. Antakelsen var gal: én TPdf per tråd er ikke isolasjon
Hvorfor er ikke én TPdf per tråd nok?
Én TPdf per tråd er ikke nok fordi PDFium beholder sin utruelige tilstand i modulen, ikke i dokumentet. Hver TPdf eier sitt eget FPDF_DOCUMENT-håndtak, men hvert håndtak i prosessen betjenes av samme lastede DLL, og den DLL-en holder prosessomfattende singletons: font-cachen, sidemodulen, og andre globale strukturer som dokumentlasting, parsing og rendering alle rører. To tråder som laster to urelaterte filer, er to tråder som skriver inn i samme font-cache samtidig. Ingen eier de dataene på Delphi-siden, så ingenting på Delphi-siden kan låse dem per dokument
Komponenten har faktisk en lås, og det er lett å trekke feil konklusjon av den. TPdf pakker sine egne renderstier inn i en intern kritisk seksjon (EnterRenderLock / LeaveRenderLock, private metoder på TPdf). Den låsen er per instans. Den stopper to tråder fra å drive samme TPdf samtidig, noe som er en reell fare, men den kan ikke se en andre instans på en annen tråd, så kryss-instans-samtidighet går rett forbi den. Den generelle regelen er enkel nok til å si på én linje: i én lastet PDFium-modul, kan høyst én tråd være inne i PDFium i ethvert øyeblikk, uansett hvor mange dokumenter som er åpne
Hvordan ser kryss-dokument-korrupsjon ut i en Delphi-prosess?
Kryss-dokument-korrupsjon ser ut som et tilfeldig miks av urelaterte feil, og skaden overlever koden som forårsaket den. Før v3.125.1 opprettet ValidatePdfFilesParallel én TPdf per arbeidertråd og kjørte Active := True pluss byggingen av preflight-rapporten samtidig på den delte modulen. Symptomene sett på både Delphi- og Free Pascal-bygg dekket hele spekteret:
- En gyldig fil feiler å laste, eller kommer tilbake fra batchen som feilet når den skulle ha bestått
- En access violation kommer til syne i et senere, urelatert kall, ofte i en annen test eller et annet dokument
External exception C000001Ddukker opp i Delphi. Den koden erSTATUS_ILLEGAL_INSTRUCTION, reist avud2-instruksjonen som PDFiums interneCHECK- ogIMMEDIATE_CRASH-makroer utfører når en invariant brytes- Prosessen avslutter med
0xC0000409(fail-fast, rapportert som stack buffer overrun) eller0xC0000374(heap corruption), uten noe Delphi-unntak i det hele tatt
De to siste punktene er grunnen til at buggen var så vanskelig å få has på. Den parallelle valideringen ble ferdig, den korrupte globale tilstanden ble igjen, og neste fixture i samme prosess snublet i den. I én Delphi Win64-regresjonskjøring rammet en bølge av C000001D-feiler tester som aldri rørte batchvalidering; de var rett og slett den første koden som brukte PDFium etter skaden. De målte tallene gjør skalaen tydelig. En Delphi-probe som kjørte samme prøve gjennom to arbeidere, feilet 122 av 160 dokumenter i én kjøring og 138 av 160 i en annen, og én av de kjøringene reiste External exception C000001D rett ut. Et stress-tilfelle på 8 dokumenter, 4 arbeidere og 5 runder feilet eller krasjet i 5 av 5 kjøringer på Free Pascal Win64. Etter fiksen feilet samme probe 0 av 1 200 dokumenter
Slik forblir ValidatePdfFilesParallel trygg siden v3.125.1
ValidatePdfFilesParallel serialiserer nå den native halvdelen av hver jobb og holder den administrerte halvdelen parallell. Hver arbeider tar én kritisk seksjon på enhetsnivå før den oppretter sin TPdf, og holder den gjennom FileName, Active := True, byggingen av preflight-rapporten, og Free. Opprettelse og ødeleggelse er inne i låsen med vilje: å lukke et dokument kaller tilbake inn i modulen akkurat som lasting gjør. Så snart arbeideren har en fangnet TPdfPreflightReport-post, slipper den låsen og evaluerer valideringsreglene mot den posten, noe som ikke rører noen PDFium-tilstand, så regelevaluering for én fil overlapper PDFium-arbeidet for den neste
To mindre endringer fulgte med fiksen. En lastefeil reiser nå EPdfError med LastLoadReport.ErrorMessage, så elementets ErrorMessage navngir det faktiske parseproblemet i stedet for en sekundær «no active document»-feil. Og kostnaden er oppgitt ærlig: PDFium-delen av batchen er nå seriell, så på en batch dominert av parsing og preflight, kjøper ekstra arbeidere lite. Er du på en versjon før v3.125.1, sett WorkerCount til 1; det fjerner samtidigheten og korrupsjonen med den
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 = prosessorantall, takket til 8
Options.Standards := [ppsPdfA];
// Med et eksplisitt register velger du selv det matchende profilen.
// En tom Profiles-liste kjører hver registrerte regel, og regler for
// standarder du ikke preflightet, rapporterer "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;
Å sende nil som register er den kortere ruten: ValidatePdfFilesParallel oppretter da standardregisteret selv, utleder profillisten fra Options.Standards, og frigjør registeret når den returnerer. Resultater kommer alltid tilbake i inndaterekkefølge, uansett rekkefølge arbeiderne ble ferdige i. For rapportformatene og kommandolinje-wrapperen rundt samme motor, se batch PDF preflight-rapporter med PDFium Component CLI, og for hva PDF/A-sjekkene selv dekker, PDF/A preflight-validering i Delphi
Hvordan kjører RenderPagesParallel sider ekte parallelt?
TPdf.RenderPagesParallel kjører parallelt fordi arbeiderne aldri deler en PDFium-modul. Metoden lagrer først det aktive dokumentet inn i et kilde-lager på kalletråden. Hver arbeider kopierer så den lastede PDFium DLL-en til en unikt navngitt fil i temp-katalogen, laster den kopien med LoadLibrary, og initialiserer den. Windows behandler en DLL lastet fra en annen sti som en annen modul, så hver kopi får sine egne globaler: sin egen font-cache, sin egen sidemodul, sitt eget alt. Arbeideren åpner det lagrede dokumentet i sin private modul, renderer sidene sine progresivt med kanselleringssjekker mellom stegene, ødelegger så biblioteket, losser kopien og sletter filen
Isolasjonen er ikke gratis, og standardverdiene gjenspeiler det. Hver arbeider betaler for en DLL-kopi på disk, et andre sett PDFium-globaler i minnet, og en fersk parsing av dokumentet. MaxWorkers = 0 betyr høyst 4 arbeidere, MaxPixelsPerPage og MaxTotalOutputBytes takker det rå utdataet, og de inverterte og natt-duotone render-alternativene avvises fordi bufferne returneres rå. Resultatet er en TPdfParallelRenderReport hvis Results-matrise holder én top-down 32-bit buffer per forespurt side, i forespørselsrekkefølge
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; // sidenumre er 1-baserte
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// Kildeøyeblikksbildet tas på den delte modulen, så hold den
// prosessomfattende PDFium-låsen hvis andre tråder også bruker 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;
Merk låsen rundt kallet. Arbeidermodulene er private, men øyeblikksbildesteget ved starten kjører SaveAs på den delte modulen fra kalletråden. Rører ingenting annet i prosessen din TPdf samtidig, kan du droppe låsen; rører noe det, trenger øyeblikksbildet samme beskyttelse som hvert annet delt-modul-kall
| Mønster | Trygt på tvers av dokumenter | PDFium-arbeid kjører parallelt | Kostnad |
|---|---|---|---|
Én TPdf per tråd, ingen delt lås | Nei | Ja, til det korrumperer | Intermitterende krasj, skadet tilstand i prosessen |
| Én prosessomfattende lås rundt alle PDFium-kall | Ja | Nei | PDFium-delen er seriell |
ValidatePdfFilesParallel siden v3.125.1 | Ja | Nei; regelevaluering er parallell | Parsing og preflight er serielle |
TPdf.RenderPagesParallel | Ja | Ja | DLL-kopi, minne og en fersk parsing per arbeider |
Hvordan bør du strukturere din egen multitrådede PDFium-kode?
Dine egne tråder bør dele én prosessomfattende lås og holde den for hele levetiden til hver TPdf de bruker, eller ellers bruke en komponent-API som isolerer modulen for deg. Låsen må være ett enkelt objekt for hele prosessen, ikke én per tråd, per skjema eller per dokument; en lås to tråder ikke deler, beskytter ingenting. Mønsteret under speiler det komponenten gjør internt siden v3.125.1: opprett, last, les og frigjør inne i låsen, og gjør alt som ikke rører PDFium utenfor den
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // én lås for hele prosessen
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; // å lukke dokumentet er også PDFium-arbeid
end;
finally
PdfiumLock.Release;
end;
// Ingen PDFium under denne linjen, så denne delen kjører parallelt
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Noen få regler holder mønsteret ærlig i en virkelig applikasjon:
- Legg
TPdf.CreateogFreeinne i låsen, ikke bare de åpenbare kallene. Lasting, lukking, egenskapslesinger somPageCount, sidebytter, tekstuttrekk, rendering og lagring når alle inn i modulen - Sjekk
Activeetter å ha tildelt den. En feilet lasting etterlaterActivepåFalse, ogLastLoadReport.ErrorMessagesier hvorfor - Hold låsen per dokument snarere enn per kall. Finere låsing er mulig i prinsippet, men bare hvis intet
TPdf-medlem noensinne kjører utenfor den, og den grove versjonen er den komponenten selv er avhengig av - Hold tregt ikke-PDFium-arbeid, som database-skrivinger, indeksering og nettverkskall, utenfor låsen, ellers vil én treg konsument serialisere alt
- Ikke behandle den private per-instans render-låsen som en erstatning. Den vokter én
TPdfmot seg selv og ingenting mer
Samme forsiktighet gjelder kode du ikke skrev som rå tråder. Bakgrunnsfutures er en god måte å holde lange renderinger vekk fra UI-tråden, som beskrevet i bakgrunnsrendering av PDF med kansellerbare futures, men future-eksekutøren legger ikke til en global PDFium-lås av seg selv. Kan flere futures drive ulike TPdf-instanser samtidig, ta den samme prosessomfattende låsen inne i hver arbeider, og behandle en viser på hovedtråden som én klient til av den delte modulen. Kryss-instans-bruk gjennom de asynkrone API-ene er ikke revidert separat, så den konservative antakelsen er at den trenger samme serialisering som håndskrevne tråder. Trenger du ekte PDFium-parallellisme til noe annet enn siderendering, gir separate arbeiderprosesser hver jobb sin egen modul av konstruksjon
Hurtigreferanse: PDFium trådingsregler for Delphi
- PDFiums utruelige tilstand er modulomfattende: font-cache, sidemodul og andre globaler deles av hvert dokument i prosessen
- Én
TPdfper tråd isolerer ingenting; to instanser på to tråder kan fortsatt korrumpere hverandre - Typiske symptomer er lastefeil, access violations i senere kode,
External exception C000001D, og avslutninger med0xC0000409eller0xC0000374 - Korrupsjonen vedvarer i prosessen, så det feilende kallet er ofte ikke det som forårsaket den
ValidatePdfFilesParalleler trygg siden v3.125.1; på eldre versjoner brukWorkerCount := 1TPdf.RenderPagesParalleler genuint parallell fordi hver arbeider laster en isolert kopi av PDFium-modulen- Dine egne tråder, tasks og futures trenger én prosessomfattende lås som dekker hver
TPdffraCreatetilFree
PDFium Component wrapper PDFium-motoren for Delphi med batch preflight og validering, isolert parallell rendering, kansellerbart bakgrunnsarbeid og detaljert lastediagnostikk. Detaljer og utgaver finner du på produktsiden for PDFium Component