PDFium är inte trådsäkert på modulnivå, så två TPdf-instanser som arbetar på två olika filer i två trådar kan fortfarande korrumpera varandra. PDFium Component for Delphi hanterar detta på två sätt: sedan v3.125.1 serialiserar ValidatePdfFilesParallel varje nativt PDFium-anrop bakom ett processövergripande lås, medan TPdf.RenderPagesParallel ger varje arbetare sin egen isolerade kopia av PDFium-modulen. Buggen som tvingade fram fixen var den värsta sortens intermittent. Ett batchvalideringstest passerade större delen av tiden, rapporterade sedan en av två bra filer som misslyckad, kraschade sedan nästa test i samma process med ett access violation, och tog ibland ner hela testlöparen med en exitkod i stället för en stackspårning. Inget var fel på testet, och inget var fel på något enskilt dokument. Antagandet var fel: en TPdf per tråd är ingen isolering
Varför räcker inte en TPdf per tråd?
En TPdf per tråd räcker inte för att PDFium håller sitt otrygga tillstånd i modulen, inte i dokumentet. Varje TPdf äger sitt eget FPDF_DOCUMENT-handtag, men varje handtag i processen betjänas av samma laddade DLL, och den DLL:en håller processövergripande singeltoner: typsnittscachen, sidmodulen och andra globala strukturer som dokumentinläsning, parsning och rendering alla rör. Två trådar som läser in två orelaterade filer är två trådar som skriver i samma typsnittscache samtidigt. Ingen äger den data på Delphi-sidan, så ingenting på Delphi-sidan kan låsa den per dokument
Komponenten har visst ett lås, och det är lätt att dra fel slutsats av det. TPdf vecklar in sina egna renderingsvägar i en intern kritisk sektion (EnterRenderLock / LeaveRenderLock, privata metoder hos TPdf). Det låset är per instans. Det hindrar två trådar från att köra samma TPdf samtidigt, vilket är en verklig risk, men det kan inte se en andra instans på en annan tråd, så samtidighet mellan instanser går rakt förbi det. Huvudregeln är enkel nog att säga på en rad: i en enda laddad PDFium-modul får som mest en tråd vara inne i PDFium i varje ögonblick, oavsett hur många dokument som är öppna
Hur ser korrumpering mellan dokument ut i en Delphi-process?
Korrumpering mellan dokument ser ut som en slumpmässig blandning av orelaterade fel, och skadan överlever koden som orsakade den. Före v3.125.1 skapade ValidatePdfFilesParallel en TPdf per arbetstråd och körde Active := True plus preflight-rapportbygget samtidigt på den delade modulen. Symptomen som setts på både Delphi- och Free Pascal-byggen täckte hela spektrumet:
- En giltig fil misslyckas att läsas in, eller kommer tillbaka från batchen som misslyckad när den borde ha passerat
- Ett access violation dyker upp i ett senare, orelaterat anrop, ofta i ett annat test eller ett annat dokument
External exception C000001Ddyker upp i Delphi. Den koden ärSTATUS_ILLEGAL_INSTRUCTION, kastad av instruktionenud2som PDFiums internaCHECK- ochIMMEDIATE_CRASH-makron kör när en invariant bryts- Processen avslutas med
0xC0000409(fail-fast, rapporterad som en stack buffer overrun) eller0xC0000374(heap corruption), utan något Delphi-undantag alls
De två sista punkterna är varför buggen var så svår att fastställa. Den parallella valideringen fullbordades, det korrumperade globala tillståndet stannade kvar, och nästa fixtur i samma process snubblade på den. I en Delphi Win64-regressionskörning träffade en våg av C000001D-fel tester som aldrig rörde batchvalidering; de var helt enkelt den första koden att använda PDFium efter skadan. De uppmätta siffrorna gör skalan uppenbar. En Delphi-sond som körde samma exempel genom två arbetare misslyckades med 122 av 160 dokument i en körning och 138 av 160 i en annan, och en av de körningarna kastade External exception C000001D rakt av. Ett stresstest med 8 dokument, 4 arbetare och 5 omgångar misslyckades eller kraschade i 5 av 5 körningar på Free Pascal Win64. Efter fixen misslyckades samma sond med 0 av 1 200 dokument
Hur ValidatePdfFilesParallel förblir säkert sedan v3.125.1
ValidatePdfFilesParallel serialiserar nu den nativa halvan av varje jobb och håller den hanterade halvan parallell. Varje arbetare tar en kritisk sektion på enhetsnivå innan den skapar sin TPdf, och håller den genom FileName, Active := True, preflight-rapportbygget och Free. Skapande och förstörelse ligger med flit inuti låset: att stänga ett dokument anropar tillbaka in i modulen precis som inläsning gör. Så snart arbetaren har en infångad TPdfPreflightReport-post släpper den låset och utvärderar valideringsreglerna mot den posten, vilket inte rör något PDFium-tillstånd, så regelutvärdering för en fil överlappar PDFium-arbetet för nästa
Två mindre ändringar kom med fixen. Ett inläsningsfel kastar nu EPdfError med LastLoadReport.ErrorMessage, så postens ErrorMessage namnger det verkliga parsproblemet i stället för ett sekundärt fel "no active document". Och kostnaden anges ärligt: PDFium-delen av batchen är nu seriell, så på en batch dominerad av parsning och preflight köper extra arbetare lite. Är du på en version före v3.125.1, sätt WorkerCount till 1; det tar bort samtidigheten och korrumperingen 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 = processorsantal, tak 8
Options.Standards := [ppsPdfA];
// Med en explicit registry väljer du själv matchande profil.
// En tom Profiles-lista kör varje registrerad regel, och regler för
// standarder du inte preflightat rapporterar "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;
Att skicka nil som registry är den kortare vägen: ValidatePdfFilesParallel skapar då standardregistret själv, härleder profillistan från Options.Standards och frigör registret när det returnerar. Resultaten kommer alltid tillbaka i inmatningsordning, vilken ordning arbetarna fullbordade i än. För rapportformaten och kommandoradswrappern runt samma motor, se batch-PDF-preflightrapporter med PDFium Component CLI, och för vad PDF/A-kontrollerna själva täcker, PDF/A-preflightvalidering i Delphi
Hur kör RenderPagesParallel sidor verkligen parallellt?
TPdf.RenderPagesParallel körs parallellt för att dess arbetare aldrig delar en PDFium-modul. Metoden sparar först det aktiva dokumentet i ett källförråd på anropstråden. Varje arbetare kopierar sedan den laddade PDFium-DLL:en till en unikt namngiven fil i temp-katalogen, laddar den kopian med LoadLibrary och initierar den. Windows behandlar en DLL laddad från en annan sökväg som en annan modul, så varje kopia får egna globaler: egen typsnittscache, egen sidmodul, eget allt. Arbetaren öppnar det sparade dokumentet i sin privata modul, renderar sina sidor successivt med avbrytskontroller mellan stegen, förstör sedan biblioteket, lossar kopian och raderar filen
Isoleringen är inte gratis, och standardvärdena speglar det. Varje arbetare betalar för en DLL-kopia på disken, en andra uppsättning PDFium-globaler i minnet och en färsk parsning av dokumentet. MaxWorkers = 0 betyder som mest 4 arbetare, MaxPixelsPerPage och MaxTotalOutputBytes taklägger råutdata, och de inverterade och natt-duotone-renderingsalternativen avvisas för buffrarna returneras råa. Resultatet är en TPdfParallelRenderReport vars Results-matris håller en topp-ned 32-bitarsbuffert per begärd sida, i begäransordning
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; // sidonummer är ettbaserade
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// Källsnapshoten tas på den delade modulen, så håll det
// processövergripande PDFium-låset om andra trådar också använder 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;
Notera låset runt anropet. Arbetarmodulerna är privata, men snapshotsteget i början kör SaveAs på den delade modulen från anropstråden. Rör inget annat i din process TPdf samtidigt kan du släppa låset; gör något det behöver snapshoten samma skydd som varje annat anrop till den delade modulen
| Mönster | Säkert mellan dokument | PDFium-arbete körs parallellt | Kostnad |
|---|---|---|---|
En TPdf per tråd, inget delat lås | Nej | Ja, tills det korrumperar | Intermittenta krascher, skadat processtillstånd |
| Ett processövergripande lås runt alla PDFium-anrop | Ja | Nej | PDFium-delen är seriell |
ValidatePdfFilesParallel sedan v3.125.1 | Ja | Nej; regelutvärdering är parallell | Parsning och preflight är seriella |
TPdf.RenderPagesParallel | Ja | Ja | DLL-kopia, minne och en färsk parsning per arbetare |
Hur ska du strukturera din egen flertrådade PDFium-kod?
Dina egna trådar ska dela ett processövergripande lås och hålla det under hela livslängden för varje TPdf de använder, eller annars använda ett komponent-API som isolerar modulen åt dig. Låset måste vara ett enda objekt för hela processen, inte ett per tråd, per form eller per dokument; ett lås som två trådar inte delar skyddar ingenting. Mönstret nedan speglar vad komponenten gör internt sedan v3.125.1: skapa, läsa in, läsa och frigöra inuti låset, och göra allt som inte rör PDFium utanför det
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // ett lås för hela 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; // att stänga dokumentet är också PDFium-arbete
end;
finally
PdfiumLock.Release;
end;
// Ingen PDFium under den här raden, så den här delen körs parallellt
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Några regler håller mönstret ärligt i en verklig applikation:
- Lägg
TPdf.CreateochFreeinuti låset, inte bara de uppenbara anropen. Inläsning, stängning, egenskapsläsningar somPageCount, sidbyten, textutdrag, rendering och sparande når alla in i modulen - Kontrollera
Activeefter att ha tilldelat den. En misslyckad inläsning lämnarActivepåFalse, ochLastLoadReport.ErrorMessagesäger varför - Håll låset per dokument i stället för per anrop. Finare låsning är möjlig i princip, men bara om ingen
TPdf-medlem någonsin körs utanför det, och den grova versionen är den komponenten själv förlitar sig på - Håll långsamt icke-PDFium-arbete, som databasskrivningar, indexering och nätverksanrop, utanför låset, annars serialiserar en långsam konsument allting
- Behandla inte det privata låset per instans för renderingen som en ersättning. Det vakar över en
TPdfmot sig själv och inget mer
Samma försiktighet gäller kod du inte skrev som råa trådar. Bakgrundsfutures är ett bra sätt att hålla långa renderningar borta från UI-tråden, som beskrivs i bakgrundsrendering av PDF med avbrytbara futures, men future-exekutorn lägger inte till något globalt PDFium-lås av egen hand. Kan flera futures driva olika TPdf-instanser samtidigt, ta samma processövergripande lås inuti varje arbetare, och behandla en visare på huvudtråden som ytterligare en klient av den delade modulen. Användning mellan instanser via de asynkrona API:erna har inte granskats separat, så det konservativa antagandet är att den behöver samma serialisering som handskrivna trådar. Behöver du verklig PDFium-parallellitet för något annat än sidrendering ger separata arbetarprocesser varje jobb sin egen modul av konstruktion
Snabbreferens: PDFium-trådregler för Delphi
- PDFiums otrygga tillstånd gäller hela modulen: typsnittscache, sidmodul och andra globaler delas av varje dokument i processen
- En
TPdfper tråd isolerar ingenting; två instanser på två trådar kan fortfarande korrumpera varandra - Typiska symptom är laddningsfel, access violations i senare kod,
External exception C000001Doch avslut med0xC0000409eller0xC0000374 - Korrumperingen består i processen, så det misslyckande anropet är ofta inte det som orsakade den
ValidatePdfFilesParallelär säkert sedan v3.125.1; på äldre versioner användWorkerCount := 1TPdf.RenderPagesParallelär genuint parallellt för varje arbetare laddar en isolerad kopia av PDFium-modulen- Dina egna trådar, uppgifter och futures behöver ett processövergripande lås som täcker varje
TPdffrånCreatetillFree
PDFium Component wrappar PDFium-motorn för Delphi med batch-preflight och validering, isolerad parallell rendering, avbrytbart bakgrundsarbete och detaljerad laddningsdiagnostik. Detaljer och utgåvor finns på produktsidan för PDFium Component