Merender halaman PDF secara paralel dari Delphi bermuara pada satu aturan: berikan setiap thread pekerja renderer-nya sendiri. losLab PDF Library menyediakan RenderPagesToFilesParallel untuk pekerjaan tersebut, menyebarkan rentang halaman di seluruh pool TTask dengan satu instans TPDFlib per pekerja, sehingga mesin multi-core mengubah pekerjaan raster batch menjadi throughput yang berskala mendekati jumlah core. Sebaliknya, jika Anda berbagi satu instans di seluruh thread, proses tidak akan melambat secara mulus, melainkan merusak memori dan crash
Ini adalah artikel yang Anda cari ketika pekerjaan malam hari harus mengubah PDF 500 halaman menjadi 500 PNG, mesin memiliki 16 core yang menganggur, dan upaya pertama Anda untuk menggunakan thread gagal di dalam GDI+. Versi singkatnya adalah bahwa keamanan thread di sini adalah properti struktural, bukan bendera yang Anda atur, dan sisa artikel ini akan membahas mengapa bentuk yang aman terlihat seperti itu dan di mana batas peningkatan kecepatan yang sebenarnya
Apakah TPDFlib thread-safe untuk rendering paralel?
Tidak, dan alasannya patut dipahami sebelum Anda merancang sistem di sekitarnya. Satu instans TPDFlib dideklarasikan untuk penggunaan single-thread, dan bagian yang kritis adalah TPDFPageTree.GetPage: ia menulis field FPagePointer bersama pada instans sebagai efek samping dari pemilihan halaman. Dua thread yang memanggil instans yang sama akan memperebutkan field tersebut, sehingga pekerja A bisa berada di tengah merender halaman 3 ketika pekerja B mengarahkan kembali pohon halaman ke halaman 40. Tidak ada dalam API yang mencegah Anda menulis kode di bawah ini, dan kode tersebut bahkan akan berjalan untuk beberapa halaman sebelum mengalami kesalahan, yang merupakan cara terburuk bagi bug seperti ini untuk berperilaku
// 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;
Kegagalan ini tidak deterministik, itulah sebabnya ia lolos dari pengujian cepat dan kemudian muncul di mesin pelanggan dengan jumlah core yang berbeda dan dokumen yang lebih berat. Juga tidak ada kunci yang dapat Anda bungkus di sekitar RenderPageToFile untuk memperbaikinya dengan mudah, karena menahan mutex di seluruh panggilan render akan membuat pekerjaan berjalan berurutan dan membuang paralelisme yang Anda cari
Mengapa setiap pekerja render membutuhkan instans TPDFlib-nya sendiri?
Karena instans tersebut adalah unit isolasi. Setelah setiap pekerja memiliki TPDFlib pribadi yang memuat file secara independen, masing-masing memiliki pohon halamannya sendiri, FPagePointer-nya sendiri, dan status rendernya sendiri, sehingga tidak ada yang dibagikan untuk diperebutkan. Keamanan itu memiliki harga yang harus Anda perhitungkan di awal: setiap pekerja mengurai seluruh dokumen ke dalam memori, sehingga penggunaan puncak kira-kira N kali biaya instans tunggal. Delapan pekerja pada PDF 300 MB berarti delapan proses penguraian penuh berada di memori secara bersamaan, dan pada input yang sangat besar, batasan itulah yang menentukan jumlah pekerja Anda, bukan CPU. Ketika dokumen sangat besar dan Anda dibatasi oleh memori alih-alih CPU, jalur akses langsung dalam memproses PDF besar tanpa penguraian dokumen lengkap sering kali merupakan solusi yang lebih baik daripada menambah thread render
API satu panggilan: RenderPagesToFilesParallel
losLab PDF Library mengemas seluruh pola aman di balik satu metode, sehingga untuk kasus umum Anda tidak perlu membuatnya secara manual. RenderPagesToFilesParallel menerima nama file dan kata sandi, DPI, halaman awal dan akhir yang inklusif, nilai Options yang diteruskan langsung ke jalur raster per halaman, pola output di mana %p diganti dengan nomor halaman, dan batas pekerja di mana nilai apa pun pada atau di bawah nol berarti otomatis. Ini mengembalikan jumlah halaman yang berhasil dirender, dan ini adalah jalur khusus Windows karena bersandar pada CoInitialize dan 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;
Mengapa CoInitialize di setiap thread pekerja?
GDI+ adalah rasterizer di bawah rendering halaman, dan GDI+ adalah apartment-threaded: ia mengharapkan COM diinisialisasi pada thread mana pun yang memanggilnya. Thread utama aplikasi VCL biasanya sudah menyiapkannya, tetapi pekerja TTask yang baru dibuat tidak, dan memanggil jalur render dari thread yang tidak diinisialisasi adalah cara pasti untuk crash. Jadi setiap pekerja memasangkan CoInitialize(nil) saat masuk dengan CoUninitialize saat keluar, mengapit seluruh masa pakainya. Ini adalah aturan yang sama yang dibutuhkan oleh pekerjaan GDI+ atau COM di luar thread utama, dan merupakan bagian kedua dari apa yang membuat isolasi per pekerja benar-benar bertahan, bagian pertamanya adalah instans pribadi. Jalur raster GDI+ yang sama menggerakkan engine single-threaded yang dibahas dalam memilih rendering engine untuk output PDF
Sharding statis versus klaim halaman dinamis
Cara yang jelas untuk membagi 500 halaman ke 8 pekerja adalah dengan memberikan masing-masing irisan tetap sekitar 62 halaman. losLab PDF Library tidak melakukan itu, dan alasannya adalah keseimbangan beban. Biaya halaman sangat bervariasi: halaman teks isi merender dalam milidetik, sedangkan halaman peta vektor yang padat atau gambar terpindai tanpa tepi (full-bleed) dapat memakan waktu lima puluh kali lebih lama. Bagi pekerjaan menjadi sharding tetap dan pekerja yang kebetulan mendapat irisan berat akan berjalan lama setelah pekerja lainnya menganggur, sehingga waktu jam dinding Anda ditentukan oleh shard yang paling tidak beruntung, bukan rata-rata. Sebaliknya, setiap pekerja mengklaim halaman berikutnya dari pencacah bersama di bawah critical section yang singkat, merendernya, dan kembali untuk halaman berikutnya, yang membuat setiap core tetap sibuk hingga seluruh rentang terkuras
// 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;
Pencatatan log terstruktur di seluruh thread pekerja
Men-debug batch yang mati pada halaman 213 dari 500 sangat menyulitkan tanpa log, dan log yang naif adalah bug konkurensi tersendiri. losLab PDF Library menyertakan TPDFlibLogger, yang dilampirkan melalui properti TPDFlib.Logger dan bernilai nil secara bawaan sehingga jalur tanpa pencatat log tetap tanpa biaya. Ini berbasis callback-first: Anda mengatur OnLog dan mengarahkan rekaman ke mana pun yang diinginkan host, difilter oleh tingkat llDebug / llInfo / llWarn / llError, dan PDFlibErrorMessage mengubah kode numerik mentah menjadi teks yang dapat dibaca manusia sehingga rekaman Error terbaca lebih dari sekadar integer biasa. Sink file opsional adalah satu-satunya sumber daya bersama, dan dilindungi oleh TCriticalSection agar beberapa pekerja dapat menambahkan data ke satu file log dengan aman. Perhatikan batasan jujurnya: hanya sink file itu yang disinkronkan, jadi jika Anda berbagi satu pencatat log di seluruh pool yang dibuat manual dan OnLog Anda menyentuh UI, Anda masih harus melakukan marsekal (marshal) kembali ke thread utama sendiri
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;
Berapa banyak peningkatan kecepatan yang sebenarnya harus Anda harapkan?
Jujurlah pada diri sendiri tentang ke mana perginya waktu, karena rendering paralel hanya menguntungkan jika pekerjaannya benar-benar dibatasi oleh CPU (CPU-bound). Output DPI tinggi dan halaman vektor atau berbayang yang kompleks sangat berat secara komputasi, dan itu berskala mendekati linier dengan jumlah core hingga Anda memenuhi kapasitas CPU. Halaman-halaman biasa adalah cerita lain: di sana overhead LoadFromFile per pekerja, ditambah biaya disk untuk menulis file output, dapat membebani rendering itu sendiri, dan delapan pekerja yang memperebutkan satu disk lambat dapat selesai lebih lambat daripada perulangan serial yang bersih. Atur MaxWorkers ke jumlah core fisik Anda daripada sesuatu yang aspirasional, awasi memori ketika sumber PDF berukuran besar, dan jika batch ternyata dibatasi oleh IO (IO-bound), solusinya adalah penyimpanan yang lebih cepat atau lebih sedikit pekerja, bukan lebih banyak thread. Digunakan pada pekerjaan yang dirancangnya, jalur render batch yang ditunjukkan di sini adalah bagian dari standar losLab PDF Library untuk Delphi dan C++Builder, dan ia mengubah core yang menganggur menjadi halaman selesai tanpa perangkap keamanan thread yang harus Anda selesaikan sendiri