PDFium modül düzeyinde thread-safe değildir; dolayısıyla iki thread'de iki farklı dosyayla çalışan iki TPdf instance'ı birbirini yine de bozabilir. Delphi için PDFium Component bunu iki yolla halleder: v3.125.1'den beri ValidatePdfFilesParallel her yerel PDFium çağrısını tek bir süreç geneli kilit ardında serileştirir; TPdf.RenderPagesParallel ise her worker'a PDFium modülünün kendi yalıtılmış kopyasını verir. Düzeltmeyi zorlayan hata, aralıklılığın en kötü türündendi. Bir batch doğrulama testi çoğu zaman geçiyor, sonra iki iyi dosyadan birini başarısız bildiriyor, sonra aynı süreçteki sonraki testi bir access violation'la çökertiyor, bazen de bütün runner'ı stack trace yerine bir exit kodla indiriyordu. Testte yanlış olan hiçbir şey yoktu ve hiçbir tek belgede de yoktu. Yanlış olan varsayımdı: thread başına bir TPdf yalıtım değildir
Thread başına bir TPdf neden yetmez?
Thread başına bir TPdf yetmez, çünkü PDFium güvensiz durumunu belgede değil modülde tutar. Her TPdf kendi FPDF_DOCUMENT handle'ına sahiptir ama süreçteki her handle aynı yüklenmiş DLL tarafından servis edilir ve o DLL süreç geneli singleton'lar tutar: font cache, page module ve belge yükleme, ayrıştırma ve render'in hepsinin dokunduğu öteki global yapılar. İki ilişkisiz dosyayı yükleyen iki thread, aynı anda aynı font cache'e yazan iki thread'dir. O veriye Delphi tarafında sahip olan yoktur; dolayısıyla Delphi tarafında onu belge başına kilitleyen hiçbir şey olamaz
Bileşenin bir kilidi vardır ve ondan yanlış sonuç çıkarmak kolaydır. TPdf, kendi render yollarını dahili bir critical section içine sarar (EnterRenderLock / LeaveRenderLock, TPdf'in private metotları). O kilit instance başınadır. Aynı anda iki thread'in aynı TPdf'yi sürmesini engeller — gerçek bir tehlikedir bu — ama başka thread'deki ikinci bir instance'ı göremez; dolayısıyla çapraz-instance eşzamanlılık düz onun yanından geçer. Genel kural tek satırda özetlenecek kadar basittir: tek bir yüklenmiş PDFium modülünde herhangi bir anda PDFium içinde en fazla bir thread olabilir, kaç belge açık olduğuna bakılmaksızın
Çapraz-belge bozulması bir Delphi sürecinde nasıl görünür?
Çapraz-belge bozulması, ilişkisiz başarısızlıkların rastgele bir karışımı gibi görünür ve hasar, ona neden olan kodu aşar. v3.125.1 öncesinde ValidatePdfFilesParallel, worker thread başına bir TPdf kuruyor ve Active := True ile preflight rapor kurulumunu paylaşılan modülde eşzamanlı koşturuyordu. Hem Delphi hem Free Pascal derlemelerinde görülen belirtiler bütün yelpazeyi kaplıyordu:
- Geçerli bir dosya yüklenemiyor ya da geçmesi gerekirken batch'ten başarısız dönüyor
- Bir access violation, sonradan ilişkisiz bir çağrıda su yüzüne çıkıyor; çoğunlukla başka bir testte ya da başka bir belgede
- Delphi'de
External exception C000001Dgörünüyor. O kodSTATUS_ILLEGAL_INSTRUCTION'dır; PDFium'un dahiliCHECKileIMMEDIATE_CRASHmakrolarının bir değişmez bozulduğunda çalıştırdığıud2komutu yükseltir - Süreç
0xC0000409(fail-fast; stack buffer overrun diye bildirilir) ya da0xC0000374(heap corruption) ile çıkıyor; hiçbir Delphi exception'ı yok
Son iki madde, hatanın neden bu kadar zor saptandığıdır. Paralel doğrulama bitiyordu, bozulmuş global durum geride kalıyordu ve aynı süreçteki sonraki fixture ona takılıyordu. Bir Delphi Win64 regresyon koşusunda bir dalga hâlindeki C000001D başarısızlıkları, batch doğrulamaya hiç dokunmamış testlere çarptı; onlar, hasardan sonra PDFium'u kullanan ilk koddan ibaretti. Ölçülen sayılar ölçeği açık eder. Aynı örneği iki worker üzerinden koşturan bir Delphi sondası bir koşuda 160 belgeden 122'sinde, başka bir koşuda 138'inde başarısız oldu ve o koşulardan biri açıkça External exception C000001D yükseltti. 8 belge, 4 worker ve 5 turdan oluşan bir stres durumu, Free Pascal Win64'te 5 koşunun 5'inde de başarısız oldu ya da çöktü. Düzeltmeden sonra aynı sonda, 1.200 belgeden 0'ında başarısız oldu
ValidatePdfFilesParallel v3.125.1'den beri nasıl güvenli kalır?
ValidatePdfFilesParallel artık her işin yerel yarısını serileştirir ve yönetilen yarısını paralel tutar. Her worker, TPdf'ini kurmadan önce bir unit düzeyi critical section alır ve onu FileName, Active := True, preflight rapor kurulumu ve Free boyunca tutar. Kurulum ve yıkım bilerek kilit içindedir: bir belgeyi kapatmak da tıpkı yüklemek gibi modüle geri çağrı yapar. Worker, yakalanmış bir TPdfPreflightReport record'una sahip olduğunda kilidi bırakır ve doğrulama kurallarını o record'a karşı değerlendirir; bu hiçbir PDFium durumuna dokunmaz, dolayısıyla bir dosyanın kural değerlendirmesi, sonrakinin PDFium işiyle örtüşür
Düzeltmeyle iki küçük değişiklik geldi. Bir yükleme başarısızlığı artık LastLoadReport.ErrorMessage ile EPdfError yükseltir; dolayısıyla öğenin ErrorMessage'i, ikincil bir "aktif belge yok" hatası yerine gerçek ayrıştırma sorununu adıyla söyler. Ve bedel dürüstçe söylenir: batch'in PDFium kısmı artık seridir; dolayısıyla ayrıştırma ve preflight'ın egemen olduğu bir batch'te fazladan worker'lar az şey alır. v3.125.1 öncesi bir sürümdeyseniz WorkerCount'i 1'e set edin; bu, eşzamanlılığı ve onunla birlikte bozulmayı kaldırır
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 = işlemci sayısı, 8'le sınırlandı
Options.Standards := [ppsPdfA];
// Açık bir registry ile eşleşen profili kendiniz seçin.
// Boş bir Profiles listesi kayıtlı her kuralı koşturur ve preflight
// etmediğiniz standartların kuralları "did not pass" bildirir
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;
Registry olarak nil geçirmek kısa yoldur: ValidatePdfFilesParallel o zaman varsayılan registry'nin kendisini kurar, profil listesini Options.Standards'tan türetir ve dönerken registry'yi serbest bırakır. Sonuçlar, worker'lar hangi sırada bitmiş olursa olsun daima girdi sırasıyla döner. Rapor biçimleri ve aynı motorun etrafındaki komut satırı sarmalayıcısı için PDFium Component CLI ile batch PDF preflight raporları'na, PDF/A kontrollerinin kendilerinin neleri kapsadığı için Delphi'de PDF/A preflight doğrulaması yazısına bakın
RenderPagesParallel sayfaları gerçekten paralel nasıl koşturur?
TPdf.RenderPagesParallel paralel koşar, çünkü worker'ları hiçbir zaman bir PDFium modülü paylaşmaz. Metot önce aktif belgeyi, çağıran thread üzerinde bir kaynak deposuna kaydeder. Sonra her worker, yüklenmiş PDFium DLL'sini geçici dizinde benzersiz adlı bir dosyaya kopyalar, o kopyayı LoadLibrary ile yükler ve başlatır. Windows, başka bir yoldan yüklenen bir DLL'yi farklı bir modül sayar; dolayısıyla her kopya kendi global'lerini alır: kendi font cache'i, kendi page module'ü, kendi her şeyi. Worker, kaydedilmiş belgeyi kendi özel modülünde açar, sayfalarını adımlar arası iptal kontrolleriyle aşamalı olarak render eder, sonra kütüphaneyi yok eder, kopyayı boşaltır ve dosyayı siler
Yalıtım bedava değildir ve varsayılanlar bunu yansıtır. Her worker, diskte bir DLL kopyasının, bellekte ikinci bir PDFium global kümesinin ve belgenin taze bir ayrıştırmasının bedelini öder. MaxWorkers = 0 en çok 4 worker demektir; MaxPixelsPerPage ile MaxTotalOutputBytes ham çıktıyı tavanlar ve tamponlar ham döndürüldüğü için inverted ile gece-duotone render seçenekleri reddedilir. Sonuç, Results dizisi istenen her sayfa için istek sırasıyla bir üstten-aşağı 32 bitlik tampon tutan bir TPdfParallelRenderReport'tur
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; // sayfa numaraları 1 tabanlıdır
Options := TPdfParallelRenderOptions.Default;
Options.Dpi := 150;
Options.MaxWorkers := 4;
// Kaynak anlık görüntüsü paylaşılan modülde alınır; öteki thread'ler de
// TPdf kullanıyorsa süreç geneli PDFium kilidini tutun
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;
Çağrının çevresindeki kilide dikkat. Worker modülleri özeldir ama baştaki anlık görüntü adımı, çağıran thread üzerinden paylaşılan modülde SaveAs koşturur. Süreçte başka hiçbir şey eşzamanlı olarak TPdf'ye dokunmuyorsa kilidi atlayabilirsiniz; bir şey dokunuyorsa anlık görüntü, öteki her paylaşılan-modül çağrısıyla aynı korumayı ister
| Örüntü | Belgeler arası güvenli | PDFium işi paralel koşar | Bedel |
|---|---|---|---|
Thread başına bir TPdf, paylaşılan kilit yok | Hayır | Evet, bozulana dek | Aralıklı çökmeler, hasarlı süreç durumu |
| Bütün PDFium çağrılarının çevresinde tek süreç geneli kilit | Evet | Hayır | PDFium kısmı seridir |
ValidatePdfFilesParallel, v3.125.1'den beri | Evet | Hayır; kural değerlendirmesi paralel | Ayrıştırma ile preflight seridir |
TPdf.RenderPagesParallel | Evet | Evet | Worker başına DLL kopyası, bellek ve taze ayrıştırma |
Kendi çok thread'li PDFium kodunuzu nasıl yapılandırmalısınız?
Kendi thread'leriniz tek bir süreç genesi kilidi paylaşmalı ve kullandıkları her TPdf'in bütün ömrü boyunca tutmalı ya da sizin için modülü yalıtan bir bileşen API'si kullanmalıdır. Kilit, bütün süreç için tek bir object olmak zorundadır; thread başına, form başına ya da belge başına değil. İki thread'in paylaşmadığı bir kilit hiçbir şeyi korumaz. Aşağıdaki örüntü, bileşenin v3.125.1'den beri içeride yaptığını aynalar: kurulumu, yüklemeyi, okumayı ve serbest bırakmayı kilit içinde; PDFium'a dokunmayan her şeyi dışında yapın
uses
System.Classes, System.SysUtils, System.SyncObjs, PDFium;
var
PdfiumLock: TCriticalSection; // bütün süreç için tek kilit
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; // belgeyi kapatmak da PDFium işidir
end;
finally
PdfiumLock.Release;
end;
// Bu satırın altında PDFium yok; dolayısıyla bu kısım paralel koşar
FText := Raw.ToString.Trim;
finally
Raw.Free;
end;
end;
initialization
PdfiumLock := TCriticalSection.Create;
finalization
PdfiumLock.Free;
Birkaç kural, örüntüyü gerçek bir uygulamada dürüst tutar:
TPdf.CreateileFree'yi kilit içine koyun; yalnızca bariz çağrıları değil. Yükleme, kapatma,PageCountgibi property okumaları, sayfa değişimleri, metin çıkarma, render ve kaydetmenin hepsi modüle uzanırActive'i atadıktan sonra kontrol edin. Başarısız bir yüklemeActive'iFalse'ta bırakır veLastLoadReport.ErrorMessagenedenini söyler- Kilidi çağrı başına değil belge başına tutun. Daha ince kilitleme ilke olarak mümkündür ama yalnızca hiçbir
TPdfüyesinin dışında koşmadığı durumlarda; bileşenin kendisinin bel bağladığı da kaba sürümdür - Veritabanı yazımları, indeksleme ve ağ çağrıları gibi yavaş PDFium-dışı işleri kilit dışında tutun; yoksa yavaş bir tüketici her şeyi serileştirir
- Özel instance başına render kilidini bir ikame olarak görmeyin. O, tek bir
TPdf'yi kendisine karşı korur, daha fazlasını değil
Aynı dikkat, ham thread olarak yazmadığınız kod için de geçerlidir. Arka plan future'ları, uzun render'ları UI thread'inin dışında tutmanın iyi bir yoludur; iptal edilebilir future'larla arka plan PDF render yazısında anlatıldığı gibi. Ama future executor'ı kendine ait bir global PDFium kilidi eklemez. Birkaç future aynı anda farklı TPdf instance'larını sürebiliyorsa her worker içinde aynı süreç geneli kilidi alın ve ana thread'deki bir görüntüleyiciyi paylaşılan modülün bir müşterisi daha sayın. Asenkron API'ler üzerinden çapraz-instance kullanım ayrıca denetlenmemiştir; dolayısıyla tutucu varsayım, elle yazılmış thread'lerle aynı serileştirmeye ihtiyaç duyduğudur. Sayfa render dışında gerçek PDFium paralelliğine ihtiyaç duyduğunuzda ayrı worker süreçleri, her işe yapım gereği kendi modülünü verir
Hızlı başvuru: Delphi için PDFium threading kuralları
- PDFium'un güvensiz durumu modül genişidir: font cache, page module ve öteki global'ler süreçteki her belge tarafından paylaşılır
- Thread başına bir
TPdfhiçbir şeyi yalıtmaz; iki thread'deki iki instance birbirini yine de bozabilir - Tipik belirtiler: yükleme başarısızlıkları, sonraki kodda access violation'lar,
External exception C000001Dve0xC0000409ya da0xC0000374ile çıkışlar - Bozulma süreçte kalıcıdır; dolayısıyla başarısız olan çağrı çoğunlukla ona neden olan değildir
ValidatePdfFilesParallelv3.125.1'den beri güvenlidir; eski sürümlerdeWorkerCount := 1kullanınTPdf.RenderPagesParallelgerçekten paraleldir; çünkü her worker, PDFium modülünün yalıtılmış bir kopyasını yükler- Kendi thread'leriniz, task'leriniz ve future'larınız, her
TPdf'iCreate'tenFree'ye kapsayan tek bir süreç geneli kilit ister
PDFium Component, PDFium motorunu Delphi için batch preflight ve doğrulama, yalıtılmış paralel render, iptal edilebilir arka plan işi ve ayrıntılı yükleme teşhisiyle sarar. Ayrıntılar ve sürümler PDFium Component ürün sayfasında