PDFium tidak thread-safe pada level module, sehingga dua instance TPdf yang mengerjakan dua file berbeda di dua thread tetap bisa saling merusak. PDFium Component for Delphi menangani ini dengan dua cara: sejak v3.125.1, ValidatePdfFilesParallel menserialisasi setiap panggilan native PDFium di balik satu lock seluruh proses, sementara TPdf.RenderPagesParallel memberi tiap worker salinan module PDFium yang terisolasi. Bug yang memaksa perbaikan itu adalah jenis intermiten yang terburuk. Test validasi batch lulus sebagian besar waktu, lalu melaporkan satu dari dua file yang baik sebagai gagal, lalu membuat test berikutnya di proses yang sama crash dengan access violation, dan kadang merobohkan seluruh runner dengan exit code alih-alih stack trace. Tak ada yang salah dengan test, dan tak ada yang salah dengan satu pun dokumen. Asumsinya yang salah: satu TPdf per thread bukan isolasi
Mengapa satu TPdf per thread tidak cukup?
Satu TPdf per thread tidak cukup karena PDFium menyimpan state tidak amannya di module, bukan di dokumen. Tiap TPdf memiliki handle FPDF_DOCUMENT-nya sendiri, tapi setiap handle di proses dilayani DLL yang termuat sama, dan DLL itu menyimpan singleton seluruh proses: font cache, page module, dan struktur global lain yang disentuh pemuatan dokumen, parsing, dan rendering. Dua thread yang memuat dua file tak berhubungan adalah dua thread yang menulis ke font cache yang sama pada saat bersamaan. Tak seorang pun memiliki data itu di sisi Delphi, sehingga tak ada apa pun di sisi Delphi yang bisa menguncinya per dokumen
Komponen memang punya lock, dan mudah menarik kesimpulan yang salah darinya. TPdf membungkus jalur render miliknya dalam critical section internal (EnterRenderLock / LeaveRenderLock, method privat TPdf). Lock itu per instance. Ia menghentikan dua thread dari menggerakkan TPdf yang sama sekaligus, bahaya yang nyata, tapi ia tak bisa melihat instance kedua di thread lain, sehingga konkurensi lintas-instance berjalan lurus melewatinya. Aturan umumnya cukup sederhana untuk dinyatakan satu baris: dalam satu module PDFium termuat, paling banyak satu thread boleh berada di dalam PDFium pada satu waktu, tak peduli berapa dokumen terbuka
Seperti apa korupsi lintas-dokumen di proses Delphi?
Korupsi lintas-dokumen tampak seperti campuran acak kegagalan tak berhubungan, dan kerusakannya hidup lebih lama dari kode yang menyebabkannya. Sebelum v3.125.1, ValidatePdfFilesParallel menciptakan satu TPdf per worker thread dan menjalankan Active := True plus pembangunan preflight report secara bersamaan di module bersama. Gejala yang terlihat di build Delphi maupun Free Pascal mencakup seluruh rentang:
- File valid gagal dimuat, atau kembali dari batch sebagai gagal padahal seharusnya lulus
- Access violation muncul di panggilan belakangan yang tak berhubungan, sering di test berbeda atau dokumen berbeda
External exception C000001Dmuncul di Delphi. Kode ituSTATUS_ILLEGAL_INSTRUCTION, dipicu instruksiud2yang dieksekusi macroCHECKdanIMMEDIATE_CRASHinternal PDFium ketika invariant pecah- Proses keluar dengan
0xC0000409(fail-fast, dilaporkan sebagai stack buffer overrun) atau0xC0000374(heap corruption), tanpa exception Delphi sama sekali
Dua poin terakhir itulah kenapa bug ini begitu sulit dikejar. Validasi paralel selesai, state global yang terkorupsi tertinggal, dan fixture berikutnya di proses yang sama tersandungnya. Dalam satu run regresi Delphi Win64, gelombang kegagalan C000001D mengenai test yang tak pernah menyentuh validasi batch; mereka sekadar kode pertama yang memakai PDFium setelah kerusakan itu. Angka terukurnya membuat skalanya jelas. Probe Delphi yang menjalankan sampel yang sama lewat dua worker gagal pada 122 dari 160 dokumen di satu run dan 138 dari 160 di run lain, dan salah satu run itu memunculkan External exception C000001D langsung. Kasus stress 8 dokumen, 4 worker, dan 5 ronde gagal atau crash di 5 dari 5 run pada Free Pascal Win64. Setelah perbaikan, probe yang sama gagal 0 dari 1.200 dokumen
Bagaimana ValidatePdfFilesParallel tetap aman sejak v3.125.1
ValidatePdfFilesParallel kini menserialisasi setengah native tiap job dan membiarkan setengah terkelolanya paralel. Setiap worker mengambil satu critical section level unit sebelum menciptakan TPdf-nya, dan memegangnya sepanjang FileName, Active := True, pembangunan preflight report, dan Free. Penciptaan dan penghancuran sengaja di dalam lock: menutup dokumen memanggil balik ke module sama seperti memuat. Begitu worker memegang record TPdfPreflightReport yang tertangkap, ia melepas lock dan mengevaluasi aturan validasi terhadap record itu, yang tak menyentuh state PDFium, sehingga evaluasi aturan satu file tumpang tindih dengan kerja PDFium file berikutnya
Dua perubahan kecil menyertai perbaikan itu. Kegagalan muat kini memicu EPdfError dengan LastLoadReport.ErrorMessage, sehingga ErrorMessage milik item menyebut masalah parse yang sesungguhnya alih-alih error sekunder "tak ada dokumen aktif". Dan biayanya dinyatakan jujur: bagian PDFium batch kini serial, jadi pada batch yang didominasi parsing dan preflight, worker ekstra nyaris tak membeli apa-apa. Kalau Anda di versi sebelum v3.125.1, set WorkerCount ke 1; itu menghapus konkurensi beserta korupsinya
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 = jumlah processor, dibatasi 8
Options.Standards := [ppsPdfA];
// Dengan registry eksplisit, pilih sendiri profil yang cocok.
// Daftar Profiles kosong menjalankan setiap rule terdaftar, dan rule untuk
// standar yang tak Anda preflight melaporkan "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;
Passing nil sebagai registry adalah jalur yang lebih pendek: ValidatePdfFilesParallel lalu menciptakan registry default sendiri, menurunkan daftar profil dari Options.Standards, dan mem-free registry saat kembali. Hasil selalu kembali dalam urutan input, apa pun urutan worker menyelesaikan. Untuk format report dan wrapper command-line di atas engine yang sama, lihat report preflight PDF batch dengan CLI PDFium Component, dan untuk cakupan pemeriksaan PDF/A-nya sendiri, validasi preflight PDF/A di Delphi
Bagaimana RenderPagesParallel menjalankan halaman benar-benar paralel?
TPdf.RenderPagesParallel berjalan paralel karena workernya tak pernah berbagi module PDFium. Method ini lebih dulu menyimpan dokumen aktif ke source store di calling thread. Setiap worker lalu menyalin DLL PDFium termuat ke file bernama unik di direktori temp, memuat salinan itu dengan LoadLibrary, dan menginisialisasinya. Windows memperlakukan DLL yang dimuat dari path berbeda sebagai module berbeda, sehingga tiap salinan mendapat global-nya sendiri: font cache sendiri, page module sendiri, semuanya sendiri. Worker membuka dokumen tersimpan di module privatnya, merender halamannya secara progresif dengan pemeriksaan pembatalan antar langkah, lalu menghancurkan library, meng-unload salinannya, dan menghapus filenya
Isolasinya tidak gratis, dan defaultnya mencerminkan itu. Tiap worker membayar salinan DLL di disk, set kedua global PDFium di memori, dan parse baru atas dokumen. MaxWorkers = 0 berarti paling banyak 4 worker, MaxPixelsPerPage dan MaxTotalOutputBytes membatasi output mentah, dan opsi render inverted serta night-duotone ditolak karena buffer dikembalikan mentah. Hasilnya adalah TPdfParallelRenderReport yang array Results-nya memuat satu buffer 32-bit top-down per halaman yang diminta, dalam urutan permintaan
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; // nomor halaman basis-1
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// Snapshot sumber diambil di module bersama, jadi pegang
// lock PDFium seluruh proses jika thread lain juga memakai 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;
Perhatikan lock di sekeliling panggilan itu. Module worker bersifat privat, tapi langkah snapshot di awal menjalankan SaveAs di module bersama dari calling thread. Kalau tak ada yang lain di proses Anda menyentuh TPdf secara bersamaan, lock bisa dilepas; kalau ada, snapshot butuh perlindungan yang sama dengan setiap panggilan shared-module lainnya
| Pola | Aman lintas dokumen | Kerja PDFium berjalan paralel | Biaya |
|---|---|---|---|
Satu TPdf per thread, tanpa lock bersama | Tidak | Ya, sampai ia korupsi | Crash intermiten, state proses rusak |
| Satu lock seluruh proses mengelilingi semua panggilan PDFium | Ya | Tidak | Bagian PDFium serial |
ValidatePdfFilesParallel sejak v3.125.1 | Ya | Tidak; evaluasi aturan paralel | Parsing dan preflight serial |
TPdf.RenderPagesParallel | Ya | Ya | Salinan DLL, memori, dan parse baru per worker |
Bagaimana seharusnya Anda menyusun kode PDFium multithread sendiri?
Thread Anda sendiri sebaiknya berbagi satu lock seluruh proses dan memegangnya sepanjang umur setiap TPdf yang dipakai, atau pakai API komponen yang mengisolasi module untuk Anda. Lock itu harus satu objek untuk seluruh proses, bukan satu per thread, per form, atau per dokumen; lock yang tak dibagi dua thread tak melindungi apa pun. Pola di bawah mencerminkan apa yang dilakukan komponen secara internal sejak v3.125.1: create, load, read, dan free di dalam lock, lalu lakukan semua yang tak menyentuh PDFium di luarnya
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // satu lock untuk seluruh proses
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; // menutup dokumen juga kerja PDFium
end;
finally
PdfiumLock.Release;
end;
// Tak ada PDFium di bawah baris ini, jadi bagian ini berjalan paralel
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Beberapa aturan menjaga pola itu tetap jujur di aplikasi nyata:
- Taruh
TPdf.CreatedanFreedi dalam lock, bukan hanya panggilan yang jelas. Memuat, menutup, pembacaan property sepertiPageCount, pergantian halaman, ekstraksi teks, rendering, dan penyimpanan semuanya menjalar ke module - Cek
Activesetelah meng-assign-nya. Muat yang gagal membiarkanActivediFalse, danLastLoadReport.ErrorMessagemengatakan kenapanya - Pegang lock per dokumen alih-alih per panggilan. Penguncian lebih halus mungkin pada prinsipnya, tapi hanya jika tak ada satu pun member
TPdfyang berjalan di luarnya, dan versi kasarnya itulah yang diandalkan komponen sendiri - Jaga kerja non-PDFium yang lambat, seperti tulis database, indexing, dan panggilan jaringan, di luar lock, atau satu consumer lambat akan menserialisasi semuanya
- Jangan memperlakukan render lock privat per-instance sebagai pengganti. Ia menjaga satu
TPdfdari dirinya sendiri dan tak lebih dari itu
Kehati-hatian yang sama berlaku untuk kode yang tak Anda tulis sebagai thread mentah. Background futures adalah cara yang bagus menjauhkan render panjang dari UI thread, seperti dijelaskan di rendering PDF latar belakang dengan cancellable futures, tapi future executor tak menambahkan lock PDFium global miliknya sendiri. Kalau beberapa futures bisa menggerakkan instance TPdf berbeda pada saat bersamaan, ambil lock seluruh proses yang sama di dalam tiap worker, dan perlakukan viewer di main thread sebagai satu klien lagi dari module bersama. Pemakaian lintas-instance lewat API asinkron belum diaudit terpisah, jadi asumsi konservatifnya adalah ia butuh serialisasi yang sama dengan thread tulisan tangan. Ketika Anda butuh paralelisme PDFium sungguhan untuk hal selain rendering halaman, worker process terpisah memberi tiap job module-nya sendiri secara konstruksi
Referensi cepat: aturan threading PDFium untuk Delphi
- State tidak aman PDFium bersifat seluruh module: font cache, page module, dan global lain dibagi oleh setiap dokumen di proses
- Satu
TPdfper thread tak mengisolasi apa pun; dua instance di dua thread tetap bisa saling merusak - Gejala tipikalnya adalah kegagalan muat, access violation di kode belakangan,
External exception C000001D, dan keluar dengan0xC0000409atau0xC0000374 - Korupsi bertahan di proses, sehingga panggilan yang gagal sering bukan yang menyebabkannya
ValidatePdfFilesParallelaman sejak v3.125.1; di versi lebih lama pakaiWorkerCount := 1TPdf.RenderPagesParallelbenar-benar paralel karena tiap worker memuat salinan terisolasi dari module PDFium- Thread, task, dan future Anda sendiri butuh satu lock seluruh proses yang meliputi tiap
TPdfdariCreatesampaiFree
PDFium Component membungkus engine PDFium untuk Delphi dengan preflight dan validasi batch, rendering paralel terisolasi, kerja latar belakang yang bisa dibatalkan, dan diagnostik muat yang rinci. Detail dan edisi ada di halaman produk PDFium Component