Rendering af PDF-sider i parallel fra Delphi bunder i én regel: Giv hver arbejder-tråd sin egen renderer. losLab PDF Library eksponerer RenderPagesToFilesParallel til netop det job, hvor et sideområde fordeles over en TTask-pulje med én TPDFlib-forekomst pr. arbejder, så en multi-core-maskine forvandler et batch-rasterjob til en gennemstrømning, der skalerer tæt på kerneantallet. Hvis du i stedet deler en enkelt forekomst på tværs af tråde, bremses kørslen ikke yndefuldt, den korrumperer hukommelsen og crasher
Dette er artiklen, du griber ud efter, når et natligt job skal forvandle en 500-siders PDF til 500 PNG'er, maskinen har 16 ledige kerner, og dit første ærlige forsøg på at tråde det væltede inde i GDI+. Den korte version er, at trådsikkerhed her er en strukturel egenskab, ikke et flag du indstiller, og resten af dette gennemgår, hvorfor den sikre form ser ud, som den gør, og hvor det reelle loft for hastighedsforøgelse faktisk befinder sig
Er TPDFlib trådsikker til parallel rendering?
Nej, og årsagen er værd at forstå, før du designer udenom den. En enkelt TPDFlib-forekomst er erklæret til enkelttrådet brug, og den skarpe kant er TPDFPageTree.GetPage: Den skriver et delt FPagePointer-felt på forekomsten som en bivirkning af at vælge en side. To tråde, der kalder ind i den samme forekomst, dyster om det felt, så arbejder A kan være halvvejs gennem side 3, når arbejder B genpeger sidetræet på side 40. Intet ved API'en forhindrer dig i at skrive koden nedenfor, og den vil endda køre i nogle få sider, før den fejler, hvilket er den værste måde for en fejl som denne at opføre sig på
// DO NOT do this: one shared instance, many threads
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
Pdf.LoadFromFile('report.pdf', '');
TParallel.For(1, Pdf.PageCount,
procedure(Page: Integer)
begin
// every thread reenters the same instance -> data race on FPagePointer
Pdf.RenderPageToFile(150, Page, 0, 'page' + IntToStr(Page) + '.png');
end);
Pdf.Free;
end;
Fejlen er ikke deterministisk, hvilket er præcis hvorfor den overlever en hurtig smoketest og derefter dukker op på en kundes maskine med et andet kerneantal og et tungere dokument. Der er heller ikke nogen lås, du kan pakke om RenderPageToFile for at løse dette billigt, fordi det at holde en mutex på tværs af hele renderkaldet serialiserer arbejdet og smider den parallelisme væk, du kom efter
Hvorfor har hver render-arbejder brug for sin egen TPDFlib-forekomst?
Fordi forekomsten er isolationsenheden. Når først hver arbejder ejer en privat TPDFlib, der indlæste filen uafhængigt, har hver sit eget sidetræ, sit eget FPagePointer og sin egen rendertilstand, så der er intet delt at dyste om. Den sikkerhed har en pris, du bør gøre op på forhånd: Hver arbejder fortolker hele dokumentet i hukommelsen, så det maksimale fodaftryk er omtrent N gange omkostningen ved en enkelt forekomst. Otte arbejdere på en 300 MB PDF is otte fulde fortolkninger indlæst på én gang, og på meget store input er det den begrænsning, der afgør dit arbejderantal, ikke CPU'en. Når dokumentet er enormt, og du er hukommelsesbegrænset snarere end CPU-begrænset, is den direkte adgangsvej i behandling af store PDF'er uden fuld dokumentfortolkning ofte et bedre håndtag end flere rendertråde
API'en med et enkelt kald: RenderPagesToFilesParallel
losLab PDF Library pakker hele det sikre mønster bag en enkelt metode, så i det almindelige tilfælde behøver du ikke selv at skrive noget af det. RenderPagesToFilesParallel tager filnavnet og adgangskoden, en DPI, en inklusiv start- og slutside, en Options-værdi sendt direkte videre til rasterstien pr. side, et outputmønster, hvor %p erstattes af sidenummeret, og et arbejderloft, hvor enhver værdi på eller under nul betyder auto. Den returnerer antallet af sider, der er renderet med succes, og det er en sti, der kun virker på Windows, fordi den læner sig op ad CoInitialize og GDI+
var
Pdf: TPDFlib;
Rendered: Integer;
begin
Pdf := TPDFlib.Create;
try
// FileName, Password, DPI, StartPage, EndPage, Options, Pattern, MaxWorkers
Rendered := Pdf.RenderPagesToFilesParallel(
'report.pdf', '', 150.0, 1, 500, 0, 'out\page_%p.png', 0);
// MaxWorkers = 0 -> auto: min(page count, CPU cores)
WriteLn(Format('%d pages rendered', [Rendered]));
finally
Pdf.Free;
end;
end;
Hvorfor CoInitialize på hver arbejder-tråd?
GDI+ er rasteringsmotoren bag siderendering, og GDI+ is apartment-threaded: Den forventer, at COM er initialiseret på den tråd, der kalder ind i den. Hovedtråden i en VCL-app har normalt allerede dette sat op, men en nyligt oprettet TTask-arbejder har ikke, og at kalde renderstien fra en uinitialiseret tråd er en pålidelig måde at crashe på. Så hver arbejder parrer en CoInitialize(nil) ved indgang med en CoUninitialize ved udgang, hvilket omslutter hele dens levetid. Dette er den samme disciplin, som alt GDI+- eller COM-arbejde har brug for uden for hovedtråden, og det er den anden halvdel af, hvad der får isolation pr. arbejder til rent faktisk at holde, hvor den første halvdel er den private forekomst. Den samme GDI+-rastersti driver de enkelttrådede motorer dækket i valg af en renderingsmotor til PDF-output
Statisk opdeling versus dynamisk sidekrav
Den indlysende måde at opdele 500 sider på 8 arbejdere er at give hver en fast del på ca. 62 sider. losLab PDF Library gør ikke det, og årsagen er belastningsfordeling. Sideomkostningerne varierer enormt: En side med brødtekst renderer på millisekunder, en side med tætte vektorkort eller et scannet billede uden margener kan tage halvtreds gange længere. Skær arbejdet i faste dele, og den arbejder, der tilfældigvis tegner den tunge del, kører længe efter, at de andre er gået i tomgang, så din tid målt på væguret bestemmes af den mest uheldige del, ikke gennemsnittet. I stedet kræver hver arbejder den næste side fra en fælles tæller under en kort kritisk sektion, renderer den og vender tilbage efter en anden, hvilket holder hver kerne travlt beskæftiget, indtil hele området er drænet
// What each worker does inside the pool (simplified)
NextPage := StartPage;
IdxLock := TCriticalSection.Create;
WorkerProc :=
procedure
var
LocalLib: TPDFlib;
PageNum: Integer;
begin
CoInitialize(nil); // GDI+ is apartment-threaded
try
LocalLib := TPDFlib.Create; // one private instance per worker
try
LocalLib.LoadFromFile(FileName, '');
while True do
begin
IdxLock.Enter; // claim the next page atomically
try
PageNum := NextPage;
Inc(NextPage);
finally
IdxLock.Leave;
end;
if PageNum > EndPage then Break;
LocalLib.RenderPageToFile(DPI, PageNum, 0,
Format('page_%d.png', [PageNum]));
end;
finally
LocalLib.Free;
end;
finally
CoUninitialize;
end;
end;
Struktureret logning på tværs af arbejder-tråde
Fejlfinding i en batch, der dør på side 213 af 500, er forfærdelig uden en log, og en simpel log er sin egen samtidighedsfejl. losLab PDF Library leverer TPDFlibLogger, tilknyttet via TPDFlib.Logger-egenskaben og nil som standard, så stien uden logning forbliver gratis. Den er callback-first: Du indstiller OnLog og dirigerer poster derhen, hvor værten ønsker det, filtreret efter et llDebug / llInfo / llWarn / llError-niveau, og PDFlibErrorMessage forvandler de rå numeriske koder til menneskelig tekst, så en Error-post læses som mere end et råt heltal. Den valgfrie fil-sink er den ene delte ressource, og den er beskyttet af en TCriticalSection netop for, at flere arbejdere kan tilføje til én logfil på en sikker måde. Bemærk den ærlige grænse: Kun den fil-sink er synkroniseret, så hvis du deler én logger på tværs af en selvbygget pulje, og din OnLog berører brugergrænsefladen, skal du stadig selv dirigere det tilbage til hovedtråden
var
Pdf: TPDFlib;
Log: TPDFlibLogger;
begin
Log := TPDFlibLogger.Create;
Log.Level := llInfo; // llDebug, llInfo, llWarn, llError
Log.FileName := 'render.log'; // optional shared sink (lock-guarded)
Log.OnLog :=
procedure(Level: TPDFlibLogLevel; Code: Integer; const Msg: WideString)
begin
if Level = llError then
// marshal to the UI thread yourself; OnLog fires on worker threads
WriteLn(Format('[%d] %s', [Code, PDFlibErrorMessage(Code)]));
end;
Pdf := TPDFlib.Create;
Pdf.Logger := Log; // nil by default; zero-cost when unset
try
Pdf.RenderPagesToFilesParallel('report.pdf', '', 150.0, 1, 500, 0,
'out\page_%p.png', 0);
// an Error now carries text, e.g. 401 -> "Wrong password or permission denied"
finally
Pdf.Free;
Log.Free;
end;
end;
Hvor stor en hastighedsforøgelse skal du egentlig forvente?
Vær ærlig over for dig selv om, hvor tiden går hen, for parallel rendering betaler sig kun, hvor arbejdet reelt er CPU-begrænset. Output med høj DPI og komplekse vektorsider eller skyggesider er beregningstunge, og disse skalerer tæt på lineært med kerneantallet, indtil du mætter CPU'en. Helt enkle sider er en anden historie: Der kan omkostningen ved LoadFromFile pr. arbejder, plus diskomkostningerne ved at skrive outputfilerne, oversvømme selve renderingen, og otte arbejdere, der kæmper om én langsom disk, kan afslutte langsommere end en ren seriel løkke. Indstil MaxWorkers to dit fysiske kerneantal frem for noget ambitiøst, hold øje med hukommelsen, når kilde-PDF'en er stor, og hvis en batch viser sig at være IO-bound, er løsningen hurtigere lagring eller færre arbejdere, ikke flere tråde. Brugt til de job, den er bygget til, er den batch-renderingssti, der vises her, en del af standarden losLab PDF Library til Delphi og C++Builder, og den forvandler ledige kerner til færdige sider uden nogen af de trådsikkerhedsfælder, du ellers selv skulle løse