Vzporedno upodabljanje strani PDF iz Delphi se zvede na eno pravilo: vsaki delovni niti dajte lasten upodabljalnik. losLab PDF Library razkriva RenderPagesToFilesParallel ravno za to delo, saj porazdeli obseg strani čez nabor TTask z eno instanco TPDFlib na delavca, tako da večjedrni stroj spremeni paketno delo rastriranja v prepustnost, ki se skalira blizu števila jeder. Če namesto tega delite eno instanco med nitmi, se izvajanje ne upočasni dostojno, temveč pokvari pomnilnik in se sesuje
To je članek, po katerem posežete, ko mora nočno opravilo pretvoriti 300-stranski PDF v 500 PNG-jev, ohišje ima 16 jeder v mirovanju, vaš prvi pošten poskus nitanja pa je padel znotraj GDI+. Kratka različica je, da je varnost niti tukaj strukturna lastnost in ne zastavica, ki jo nastavite, preostanek tega pa opisuje, zakaj je varna oblika takšna, kot je, in kje dejansko leži meja realnega pospeška
Ali je TPDFlib varen za niti pri vzporednem upodabljanju?
Ne, in razlog je vredno razumeti, preden načrtujete okoli njega. Ena sama instanca TPDFlib je deklarirana za enonitno uporabo, oster rob pa je TPDFPageTree.GetPage: kot stranski učinek izbire strani zapiše skupno polje FPagePointer na instanci. Dve niti, ki kličeta isto instanco, tekmujeta na tem polju, tako da je delavec A lahko na polovici strani 3, ko delavec B ponovno usmeri drevo strani na stran 40. Nič v API vam ne preprečuje pisanja spodnje kode, in se bo celo izvajala nekaj strani, preden bo prišlo do napake, kar je najslabši možni način za vedenje tovrstne hrošče
// 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;
Failure ni determinističen, kar je natanko tisti razlog, zakaj preživi kratek preizkus delovanja in se nato pojavi na strankinem računalniku z drugačnim številom jeder in težjim dokumentom. Prav tako ni ključavnice (lock), s katero bi lahko varno in poceni obdali RenderPageToFile, saj držanje mutexa skozi celoten klic upodabljanja serializira delo in zavrže vzporednost, zaradi katere ste sploh prišli
Zakaj vsak delavec upodabljanja potrebuje svojo instanco TPDFlib?
Ker je instanca enota izolacije. Ko vsak delavec ima v lasti zasebni TPDFlib, ki je datoteko naložil neodvisno, ima vsak svoje drevo strani, svoje polje FPagePointer in svoje stanje upodabljanja, tako da ni ničesar skupnega, na čemer bi lahko tekmovali. Ta varnost ima svojo ceno, ki jo morate oceniti vnaprej: vsak delavec razčleni celoten dokument v pomnilnik, tako da je največji odtis približno N-kratnik stroška ene instance. Osem delavcev na 300 MB velikem PDF pomeni osem celotnih razčlenitev v pomnilniku hkrati, in pri zelo velikih vhodih je to omejitev, ki določa število delavcev, ne procesor. Ko je dokument ogromen in ste omejeni s pomnilnikom namesto s procesorjem, je pot neposrednega dostopa v obdelavi velikih PDF brez popolnega razčlenjevanja dokumenta pogosto boljši vzvod kot več niti upodabljanja
Enoklicni API: RenderPagesToFilesParallel
losLab PDF Library pakira celoten varen vzorec za eno samo metodo, tako da vam za običajen primer ni treba ročno pisati ničesar od tega. RenderPagesToFilesParallel vzeme ime datoteke in geslo, DPI, vključujočo začetno in končno stran, vrednost Options, posredovano neposredno v pot rastriranja na posamezno stran, izhodni vzorec, kjer se %p nadomesti s številko strani, in omejitev delavcev, kjer katera koli vrednost pri ali pod ničlo pomeni samodejno. Vrne število uspešno upodobljenih strani in je pot le za Windows, saj se opira na CoInitialize in 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;
Zakaj CoInitialize na vsaki delovni niti?
GDI+ je rastrirnik pod upodabljanjem strani in GDI+ is apartmajsko nitan: pričakuje, da se COM inicializira na tisti niti, ki kliče vanj. Glavna nit aplikacije VCL ima to običajno že nastavljeno, sveže sprožen delavec TTask pa ne, in klicanje poti upodabljanja iz neinicializirane niti je zanesljiv način za sesutje. Zato vsak delavec združi CoInitialize(nil) ob vstopu s CoUninitialize ob izstopu, kar zaobjame celotno njegovo življenjsko dobo. To je enaka disciplina, kot jo potrebuje katero koli delo z GDI+ ali COM izven glavne niti, in je druga polovica tistega, kar dejansko zagotavlja izolaciju na delavca, prva polovica pa je zasebna instanca. Isti rastrirnik GDI+ poganja enonitne pogone, obravnavane v izbiri pogona za upodabljanje za izhod PDF
Statično drobljenje (sharding) nasproti dinamičnemu prevzemanju strani
Očitno pot do razdelitve 500 strani med 8 delavcev je, da vsakemu predate fiksni del z okoli 62 stranmi. losLab PDF Library tega ne počne, in razlog je uravnoteženje obremenitve. Strošek strani se močno razlikuje: stran z besedilom se izriše v milisekundah, stran z gostimi vektorskimi zemljevidi ali poskenirana slika pa lahko traja petdesetkrat dlje. Razrežite delo na fiksne dele in delavec, ki mu pripade težak del, teče še dolgo po tem, ko so drugi že v mirovanju, tako da vaš čas določa najmanj srečen del, ne povprečje. Namesto tega vsak delavec zahteva naslednjo stran iz skupnega števca pod kratkim kritičnim odsekom, jo upodobi in se vrne po naslednjo, katero ohranja vsako jedro zaposleno, dokler se celotno območje ne izprazni
// 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;
Strukturirano zapisovanje dnevnikov čez delovne niti
Iskanje napak v paketu, ki umre na strani 213 od 500, je brez dnevnika bedno, preprost dnevnik pa je sam po sebi hrošč sočasnosti. losLab PDF Library ponuja TPDFlibLogger, povezan prek lastnosti TPDFlib.Logger, ki je privzeto nil, tako da pot brez dnevnika ostaja brez stroškov. Temelji na povratnih klicih: nastavite OnLog in preusmerite zapise, kamor koli gostitelj želi, filtrirano po ravneh llDebug / llInfo / llWarn / llError, PDFlibErrorMessage pa pretvori surove številčne kode v človeško besedilo, tako da zapis Error pomeni več kot le golo celo število. Izbirni ponor datoteke je edini skupni vir in je zaščiten s TCriticalSection ravno zato, da lahko več delavcev varno dodaja v eno datoteko dnevnika. Upoštevajte pošteno mejo: sinhroniziran je le ta ponor datoteke, tako da če delite en zapisovalnik dnevnikov čez ročno izdelan nabor in vaš OnLog vpliva na uporabniški vmesnik, morate to še vedno sami prenesti nazaj na glavno nit
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;
Kolikšen pospešek bi dejansko morali pričakovati?
Bodite pošteni do sebe glede tega, kam gre čas, saj se vzporedno upodabljanje izplača le tam, kjer je delo resnično omejeno s procesorjem. Izhod z visokim DPI in zapletene vektorske ali senčene strani so računsko zahtevni in se skalirajo skoraj linearno s številom jeder, dokler ne nasitite procesorja. Enostavne strani so druga zgodba: tam lahko režijski stroški LoadFromFile na delavca, skupaj s stroškom pisanja izhodnih datotek na disk, preplavijo samo upodabljanje, in osem delavcev, ki se zaganjajo v en počasen disk, lahko zaključi počasneje kot čista serijska zanka. Nastavite MaxWorkers na vaše fizično število jeder namesto na nekaj ambicioznega, spremljajte pomnilnik, ko je vir PDF velik, in če se izkaže, da je paket omejen z vhodom/izhodom (IO), je rešitev hitrejša shramba ali manj delavcev, ne pa več niti. Uporabljena za dela, za katera je bila zgrajena, je tukaj prikazana pot paketnega upodabljanja del standardne knjižnice losLab PDF Library za Delphi in C++Builder in spreminja prosta jedra v dokončane strani brez kakršnih koli pasti glede varnosti niti, ki bi jih sicer morali reševati sami