Att rendera PDF-sidor parallellt från Delphi kokar ner till en regel: ge varje arbetstråd dess egen renderare. losLab PDF Library tillhandahåller RenderPagesToFilesParallel för just det jobbet, vilket sprider ett sidintervall över en pool av TTask med en TPDFlib-instans per arbetstråd. Detta gör att en flerkärnig maskin förvandlar ett batch-rasteriseringsjobb till en genomströmning som skalar nära antalet kärnor. Om du istället delar en enda instans mellan trådar blir körningen inte bara långsammare, den korrumperar minnet och kraschar
Detta är artikeln du söker när ett nattligt jobb måste förvandla en 500-sidors PDF till 500 PNG-bilder, maskinen har 16 lediga kärnor och ditt första ärliga försök att tråda det föll platt inuti GDI+. Den korta versionen är att trådsäkerhet här är en strukturell egenskap, inte en flagga du ställer in, och resten av denna text går igenom varför den säkra strukturen ser ut som den gör och var gränsen för den faktiska hastighetsökningen egentligen går
Är TPDFlib trådsäkert för parallell rendering?
Nej, och anledningen är värd att förstå innan du designar runt den. En enskild TPDFlib-instans är deklarerad för entrådig användning, och den skarpa kanten är TPDFPageTree.GetPage: den skriver till ett delat FPagePointer-fält på instansen som en bieffekt av att wybrać en sida. Två trådar som anropar samma instans tävlar om det fältet, så arbetstråd A kan vara halvvägs genom sida 3 när arbetstråd B pekar om sidträdet till sida 40. Ingenting i API:et hindrar dig från att skriva koden nedan, och den kommer till och med att köras under några sidor innan den kraschar, vilket är det sämsta sättet för en sådan här bugg att bete 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;
Felet är inte deterministiskt, vilket är precis varför det överlever ett snabbt test och sedan dyker upp på en kunds maskin med ett annat antal kärnor och ett tyngre dokument. Det finns inte heller något lås du kan lägga runt RenderPageToFile för att lösa detta billigt, eftersom att hålla ett mutex över hela renderanropet sekventialiserar arbetet och kastar bort parallellismen du ville ha
Varför behöver varje renderarbetare en egen TPDFlib-instans?
Eftersom instansen är isoleringsenheten. När varje arbetare äger en privat TPDFlib som läste in filen självständigt, har varje arbetare sitt eget sidträd, sitt eget FPagePointer och sitt eget renderingstillstånd, så det finns inget delat att tävla om. Den säkerheten har ett pris som du bör beräkna i förväg: varje arbetare läser in hela dokumentet i minnet, så den maximala minnesanvändningen är ungefär N gånger kostnaden för en enskild instans. Åtta arbetare på en PDF på 300 MB innebär åtta fullständiga analyser i minnet samtidigt, och på mycket stora filer är det denna begränsning som avgör ditt antal arbetare, inte processorn. När dokumentet är enormt och du är minnesbegränsad snarare än processorbegränsad är den direkta åtkomstvägen i bearbetning av stora PDF-filer utan fullständig dokumentanalys ofta ett bättre alternativ än fler renderingstrådar
API:et med ett anrop: RenderPagesToFilesParallel
losLab PDF Library paketerar hela det säkra mönstret bakom en enda metod, så för det vanliga fallet behöver du inte bygga något av det själv. RenderPagesToFilesParallel tar filnamnet och lösenordet, en DPI, en inkluderande start- och slutsida, ett Options-värde som skickas direkt till rasterbanan per sida, ett utdatamönster där %p ersätts med sidnumret, och en gräns för arbetare där alla värden vid eller under noll innebär automatisk inställning. Den returnerar antalet sidor som renderats framgångsrikt, och det är en ren Windows-bana eftersom den lutar sig mot CoInitialize och 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;
Varför CoInitialize på varje arbetstråd?
GDI+ är rasteriseraren under sidrendering, och GDI+ använder lägenhetsmodellering för trådar (apartment-threaded): den förväntar sig att COM är initierat på den tråd som anropar den. Huvudtråden i en VCL-applikation har vanligtvis redan detta inställt, men en nyskapad TTask-arbetare har det inte, och att anropa renderingsbanan från en oinitierad tråd är ett säkert sätt att krascha på. Så varje arbetare parar en CoInitialize(nil) vid start med en CoUninitialize vid avslut, vilket ramar in hela dess livstid. Detta är samma disciplin som allt GDI+- eller COM-arbete behöver utanför huvudtråden, och det är den andra halvan av vad som gör att isoleringen per arbetare faktiskt håller, där den första halvan är den privata instansen. Samma GDI+-rasteriseringsbana driver de entrådiga motorerna som beskrivs i att välja en renderingsmotor för PDF-utdata
Statisk uppdelning kontra dynamisk sidadressering
Det uppenbara sättet att dela upp 500 sidor på 8 arbetare är att ge var och en en fast del på cirka 62 sidor. losLab PDF Library gör inte så, och anledningen är lastbalansering. Sidkostnaden varierar enormt: en sida med löpande text renderas på några millisekunder, medan en sida med täta vektorkartor eller en skannad bild kan ta femtio gånger längre tid. Dela upp arbetet i fasta delar och arbetaren som råkar rita den tunga delen kommer att köra långt efter att de andra har blivit klara, så din totala tid bestäms av den tyngsta delen, inte genomsnittet. Istället gör varje arbetare anspråk på nästa sida från en delad räknare under en kort kritisk sektion, renderar den och kommer tillbaka för en ny, vilket håller varje kärna upptagen tills hela intervallet är avklarat
// 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;
Strukturerad loggning över arbetstrådar
Att felsöka en batch som dör på sida 213 av 500 är eländigt utan en logg, och en naiv logg är ett eget trådproblem. losLab PDF Library levereras med TPDFlibLogger, kopplad via egenskapen TPDFlib.Logger och satt till nil som standard så att banan utan logg förblir kostnadsfri. Den är callback-baserad: du ställer in OnLog och skickar poster dit värden önskar, filtrerade efter nivån llDebug / llInfo / llWarn / llError, och PDFlibErrorMessage förvandlar de råa numeriska koderna till läsbar text så att en Error-post innehåller mer än bara ett heltal. Den valfria filskrivningen är den enda delade resursen, och den skyddas av en TCriticalSection just för att flera arbetare ska kunna skriva till samma loggfil på ett säkert sätt. Observera den ärliga gränsen: endast denna filskrivning är synkroniserad, så om du delar en loggare över en manuellt skapad pool och din OnLog rör gränssnittet, måste du fortfarande själv skicka tillbaka detta till huvudtrå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;
Hur mycket hastighetsökning bör du egentligen förvänta dig?
Var ärlig mot dig själv om vart tiden tar vägen, eftersom parallell rendering endast lönar sig där arbetet verkligen är processorbegränsat. Högupplöst utdata och komplexa vektor- eller tonade sidor är beräkningsintensiva, och dessa skalar nästan linjärt med antalet kärnor tills du mättar processorn. Enkla sidor är en annan historia: där kan den extra belastningen av LoadFromFile per arbetare, plus diskkostnaden för att skriva utdatafilerna, överskugga själva renderingen, och åtta arbetare som slåss om en långsam hårddisk kan bli klara långsammare än en ren sekventiell loop. Ställ in MaxWorkers till dina fysiska kärnor snarare än något önsketänkande, håll ett öga på minnet när käll-PDF-filen är stor, och om en batch visar sig vara begränsad av in- och utmatning (IO-bound) är lösningen snabbare lagring eller färre arbetare, inte fler trådar. Använd på de jobb den byggdes för, är den batchrenderingsbana som visas här en del av standardbiblioteket losLab PDF Library för Delphi och C++Builder, och den förvandlar lediga kärnor till färdiga sidor helt utan de trådsäkerhetsfällor som du annars skulle behöva lösa själv