PDFium er ikke trådsikker på modulniveau, så to TPdf-instanser, der arbejder på to forskellige filer i to tråde, kan stadig korruptere hinanden. PDFium Component til Delphi håndterer det på to måder: siden v3.125.1 serialiserer ValidatePdfFilesParallel hvert nativt PDFium-kald bag én procesomfattende lås, mens TPdf.RenderPagesParallel giver hver worker sin egen isolerede kopi af PDFium-modulet. Buggen, der tvang fixet frem, var den værst tænkelige slags intermitterende. En batchvalideringstest bestod det meste af tiden, rapporterede så én af to gode filer som fejlet, crashede derefter næste test i samme proces med en access violation og tog nogle gange hele runneren ned med en exit-kode i stedet for en stack trace. Intet var galt med testen, og intet var galt med noget enkelt dokument. Antagelsen var forkert: én TPdf pr. tråd er ikke isolation
Hvorfor er én TPdf pr. tråd ikke nok?
Én TPdf pr. tråd er ikke nok, fordi PDFium holder sin utrygge tilstand i modulet, ikke i dokumentet. Hver TPdf ejer sin egen FPDF_DOCUMENT-handle, men hver handle i processen betjenes af den samme indlæste DLL, og den DLL holder procesomfattende singletons: font-cachen, page-modulet og andre globale strukturer, som dokumentindlæsning, parsing og rendering alle rører. To tråde, der indlæser to urelaterede filer, er to tråde, der skriver i samme font-cache på samme tid. Ingen ejer de data på Delphi-siden, så intet på Delphi-siden kan låse dem pr. dokument
Komponenten har da også en lås, og det er let at drage den forkerte konklusion ud af den. TPdf wrapper sine egne render-veje i en intern critical section (EnterRenderLock / LeaveRenderLock, private metoder på TPdf). Låsen er pr. instans. Den stopper to tråde i at drive samme TPdf på én gang, hvilket er en reel fare, men den kan ikke se en anden instans på en anden tråd, så cross-instance-concurrency går direkte forbi den. Den generelle regel er enkel nok til at sige i én linje: i ét indlæst PDFium-modul må højst én tråd være inde i PDFium på ethvert tidspunkt, uanset hvor mange dokumenter der er åbne
Hvordan ser cross-document-korruption ud i en Delphi-proces?
Cross-document-korruption ser ud som en tilfældig blanding af urelaterede fejl, og skaden overlever koden, der forårsagede den. Før v3.125.1 oprettede ValidatePdfFilesParallel én TPdf pr. worker-tråd og kørte Active := True plus preflight-rapportbygning samtidigt på det delte modul. Symptomerne set på både Delphi- og Free Pascal-builds dækkede hele spektret:
- En gyldig fil fejler at indlæse, eller kommer tilbage fra batchen som fejlet, når den burde have bestået
- En access violation kommer frem i et senere, urelateret kald, ofte i en anden test eller et andet dokument
External exception C000001Doptræder i Delphi. Den kode erSTATUS_ILLEGAL_INSTRUCTION, udløst afud2-instruktionen, som PDFiums interneCHECK- ogIMMEDIATE_CRASH-makroer eksekverer, når en invariant brydes- Processen exiter med
0xC0000409(fail-fast, rapporteret som stack buffer overrun) eller0xC0000374(heap corruption), uden nogen Delphi-exception overhovedet
De sidste to punkter er grunden til, at buggen var så svær at få greb om. Den parallelle validering fuldførte, den korrupterede globale tilstand blev tilbage, og næste fixture i samme proces snublede i den. I én Delphi Win64-regressionskørsel ramte en bølge af C000001D-fejl tests, der aldrig rørte batchvalidering; de var simpelthen den første kode, der brugte PDFium efter skaden. De målte tal gør skalaen klar. En Delphi-probe, der kørte det samme eksempel gennem to workers, fejlede 122 af 160 dokumenter i én kørsel og 138 af 160 i en anden, og én af de kørsler udløste External exception C000001D på stedet. Et stresstilfælde med 8 dokumenter, 4 workers og 5 runder fejlede eller crashede i 5 af 5 kørsler på Free Pascal Win64. Efter fixet fejlede samme probe 0 af 1.200 dokumenter
Hvordan ValidatePdfFilesParallel forbliver sikker siden v3.125.1
ValidatePdfFilesParallel serialiserer nu den native halvdel af hvert job og holder den managede halvdel parallel. Hver worker tager én unit-niveau critical section, før den opretter sin TPdf, og holder den gennem FileName, Active := True, preflight-rapportbygningen og Free. Oprettelse og destruktion er med vilje inde i låsen: at lukke et dokument kalder tilbage ind i modulet ligesom indlæsning. Så snart workeren har en fanget TPdfPreflightReport-record, slipper den låsen og evaluerer valideringsreglerne mod den record, som ikke rører nogen PDFium-tilstand, så regelevaluering for én fil overlapper PDFium-arbejdet for den næste
To mindre ændringer fulgte med fixet. En indlæsningsfejl udløser nu EPdfError med LastLoadReport.ErrorMessage, så elementets ErrorMessage nævner det faktiske parseproblem i stedet for en sekundær "no active document"-fejl. Og omkostningen nævnes ærligt: PDFium-delen af batchen er nu serial, så på en batch domineret af parsing og preflight køber ekstra workers lidt. Er du på en version før v3.125.1, så sæt WorkerCount til 1; det fjerner concurrency og korruptionen 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 = processortal, lofter ved 8
Options.Standards := [ppsPdfA];
// Med et eksplicit registry vælger du selv det matchende profil.
// En tom Profiles-liste kører hver registrerede regel, og regler for
// standarder, du ikke preflightede, 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;
At sende nil som registry er den kortere vej: ValidatePdfFilesParallel opretter så selv default-registryet, udleder profillisten fra Options.Standards og frigør registryet, når den returnerer. Resultater kommer altid tilbage i input-orden, uanset i hvilken orden workers fuldførte. For rapportformaterne og kommandolinje-wrapperen omkring samme motor, se batch PDF preflight-rapporter med PDFium Component CLI, og for hvad PDF/A-tjekkene selv dækker, PDF/A preflight-validering i Delphi
Hvordan kører RenderPagesParallel sider virkelig parallelt?
TPdf.RenderPagesParallel kører parallelt, fordi dens workers aldrig deler et PDFium-modul. Metoden gemmer først det aktive dokument i et kilde-lager på kaldende tråd. Hver worker kopierer derefter den indlæste PDFium-DLL til en unikt navngivet fil i temp-mappen, indlæser den kopi med LoadLibrary og initialiserer den. Windows behandler en DLL indlæst fra en anden sti som et andet modul, så hver kopi får sine egne globals: sin egen font-cache, sit eget page-modul, sit eget alt. Workeren åbner det gemte dokument i sit private modul, renderer sine sider progressivt med annulleringstjek mellem trin, ødelægger derefter biblioteket, aflæsser kopien og sletter filen
Isolationen er ikke gratis, og defaulterne afspejler det. Hver worker betaler for en DLL-kopi på disken, et andet sæt PDFium-globals i hukommelsen og en frisk parsing af dokumentet. MaxWorkers = 0 betyder højst 4 workers, MaxPixelsPerPage og MaxTotalOutputBytes lofter det rå output, og de inverted- og night-duotone-render-options afvises, fordi bufferne returneres rå. Resultatet er en TPdfParallelRenderReport, hvis Results-array holder én top-down 32-bit buffer pr. forespurgt side, i forespørgsels-orden
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-baserede
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// Kilde-snapshottet tages på det delte modul, så hold den
// procesomfattende PDFium-lås, hvis andre tråde også bruger 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;
Bemærk låsen omkring kaldet. Worker-modulerne er private, men snapshot-trinnet i starten kører SaveAs på det delte modul fra den kaldende tråd. Rører intet andet i din proces TPdf samtidigt, kan du droppe låsen; gør noget det, behøver snapshotet samme beskyttelse som ethvert andet shared-module-kald
| Mønster | Sikkert på tværs af dokumenter | PDFium-arbejde kører parallelt | Omkostning |
|---|---|---|---|
Én TPdf pr. tråd, ingen delt lås | Nej | Ja, indtil det korrupterer | Intermittente crashes, beskadiget proces-tilstand |
| Én procesomfattende lås omkring alle PDFium-kald | Ja | Nej | PDFium-delen er serial |
ValidatePdfFilesParallel siden v3.125.1 | Ja | Nej; regelevaluering er parallel | Parsing og preflight er serial |
TPdf.RenderPagesParallel | Ja | Ja | DLL-kopi, hukommelse og en frisk parsing pr. worker |
Hvordan bør du strukturere din egen multitrådede PDFium-kode?
Dine egne tråde bør dele én procesomfattende lås og holde den gennem hele levetiden af hver TPdf, de bruger, eller ellers bruge et komponent-API, der isolerer modulet for dig. Låsen skal være ét enkelt objekt for hele processen, ikke én pr. tråd, pr. form eller pr. dokument; en lås, to tråde ikke deler, beskytter ingenting. Mønsteret nedenfor spejler, hvad komponenten gør internt siden v3.125.1: opret, indlæs, læs og frigør inde i låsen, og gør alt, der ikke rører PDFium, uden for den
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // én lås for hele processen
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; // at lukke dokumentet er også PDFium-arbejde
end;
finally
PdfiumLock.Release;
end;
// Ingen PDFium under denne linje, så denne del kører parallelt
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Et par regler holder mønsteret ærligt i en reel applikation:
- Sæt
TPdf.CreateogFreeinde i låsen, ikke kun de åbenlyse kald. Indlæsning, lukning, property-læsninger somPageCount, sideskift, tekstudtrækning, rendering og gemning når alle ind i modulet - Tjek
Activeefter tildeling. En fejlet indlæsning efterladerActivepåFalse, ogLastLoadReport.ErrorMessagesiger hvorfor - Hold låsen pr. dokument frem for pr. kald. Finere låsning er mulig i princippet, men kun hvis intet
TPdf-medlem nogensinde kører uden for den, og den grove version er den, komponenten selv stoler på - Hold langsomt ikke-PDFium-arbejde, som database-skrivninger, indeksering og netværkskald, uden for låsen, ellers serialiserer én langsom forbruger alt
- Behandl ikke den private per-instans render-lås som en erstatning. Den vogter én
TPdfmod sig selv og ikke mere
Samme forsigtighed gælder kode, du ikke selv skrev som rå tråde. Baggrunds-futures er en god måde at holde lange renders væk fra UI-tråden, som beskrevet i baggrunds-rendering af PDF'er med annullerbare futures, men future-executoren tilføjer ikke nogen global PDFium-lås af egen art. Kan flere futures drive forskellige TPdf-instanser på samme tid, så tag samme procesomfattende lås inde i hver worker, og behandl en viewer på hovedtråden som endnu en klient af det delte modul. Cross-instance-brug gennem de asynkrone API'er er ikke separat auditeret, så den konservative antagelse er, at den behøver samme serialisering som håndskrevne tråde. Behøver du ægte PDFium-parallelisme til noget andet end side-rendering, giver separate worker-processer hvert job sit eget modul ved konstruktion
Hurtig reference: PDFium-threading-regler for Delphi
- PDFiums utrygge tilstand er modulomfattende: font-cache, page-modul og andre globals deles af hvert dokument i processen
- Én
TPdfpr. tråd isolerer ingenting; to instanser på to tråde kan stadig korruptere hinanden - Typiske symptomer er indlæsningsfejl, access violations i senere kode,
External exception C000001Dog exits med0xC0000409eller0xC0000374 - Korruptionen persisterer i processen, så det fejlende kald er ofte ikke det, der forårsagede den
ValidatePdfFilesParalleler sikker siden v3.125.1; på ældre versioner brugWorkerCount := 1TPdf.RenderPagesParalleler genuint parallel, fordi hver worker indlæser en isoleret kopi af PDFium-modulet- Dine egne tråde, tasks og futures behøver én procesomfattende lås, der dækker hver
TPdffraCreatetilFree
PDFium Component wrapper PDFium-motoren til Delphi med batch-preflight og validering, isoleret parallel rendering, annullerbar baggrundsarbejde og detaljeret indlæsningsdiagnostik. Detaljer og udgaver står på produktsiden for PDFium Component