PDFium'da bir sayfayı render etmek senkrondur. Kütüphaneye çağrı yaparsınız, o verdiğiniz bitmap'e rasterleştirir ve kontrol, pikseller yazıldığında geri gelir. Tek bir ekran boyutunda sayfa, tek bir yakınlaştırma düzeyinde birkaç milisaniye sürer ve kimse fark etmez. 200 sayfalık bir belgenin 300 dpi dışa aktarımı ya da her sayfayı aynı anda rasterleştirmek zorunda olan bir küçük resim şeridi için aynı çağrı saniyelerine mal olur. Bu çağrıyı ana iş parçacığından yaparsanız mesaj döngüsü durur, pencere yeniden çizmeyi bırakır ve Windows başlık çubuğunuzun üzerine o korkunç "Not Responding" yazısını boyar. İş doğru; yanlış olan, onu çalıştırdığınız yerdir
Çözüm, uzun render işini bir arka plan iş parçacığına taşımak ve sonucu bitmap'in bir kontrole verilebildiği ana iş parçacığına geri getirmektir. PDFium bunu yapmanıza engel değildir ama devir teslimi güvenli kılması bağlayıcı katmanın işidir; çünkü "işçide çalıştır, arayüzde yanıtla" etrafındaki hata yüzeyi geniştir ve arızalar aralıklı gelir. PDFiumPas'taki FPdfAsync birimi, bu örüntüye tek bir doğru uygulama sunmak için vardır; üstelik uzun bir renderin gerçekte nasıl davrandığına uyan bir iptal modeliyle birlikte
İşin şekli
Üç işlem, bir renderin bir kareden uzun sürdüğü durumları domine eder. Toplu render bir sayfa aralığını gezer ve her sayfayı — genellikle diske — rasterleştirir. Çok sayfalı dışa aktarım aynısını yapar ama çıktıyı tek bir dosyada birleştirir. Arka plan sayfa renderi, kullanıcı henüz önbellekte olmayan bir sayfaya atladığında bir görüntüleyicinin yaptığı işidir; bitmap iş parçacığı dışında üretilir ve hazır olduğunda gösterilir. Üçü de aynı kısıtları paylaşır. UI iş parçacığının barındıramayacağı kadar uzun çalışırlar, UI iş parçacığının er geç ihtiyaç duyduğu bir sonuç üretirler ve kullanıcı bunlardan vazgeçebilir. Belgeyi kapatmak, sayfayı geçmek veya İptal'e basmak, kullanıcıyı artık istemediği bir çıktıyı bekletmek yerine işi durdurmalıdır
Tasarımı biçimlendiren kısıt en sonuncusudur. İptal edilemeyen bir render, belgeyi açık tutan ve cevap önemini yitirdikten sonra da CPU yakan bir renderdir. Bu yüzden birim, birbiriyle kurgulanan iki ilkelin etrafında kurulmuştur: sonucu geriye taşıyan bir future ve iptal talebini ileriye taşıyan bir belirteç
Fire-and-forget bir future
TPdfFuture<T>.Run bir işçi, bir yanıt ve isteğe bağlı bir iptal belirteci alır. İşçiyi bir arka plan iş parçacığında başlatır ve işçi bittiğinde yanıtı ana iş parçacığında iletir. T genel parametresi, render'in ürettiği şeydir; çoğunlukla bir bitmap tanıtıcısı ya da bir durum kaydı. İşçi iş parçacığı dışında çalışır; yanıt, VCL'ye dokunmanın güvenli olduğu yerde çalışır
class procedure TPdfFuture<T>.Run(
const AWorker: TPdfFutureWorker<T>;
const AReply: TPdfFutureReply<T>;
const AToken: IPdfCancellationToken = nil); static;
Kasıtlı olarak eksik bırakılan şey, herhangi bir türde Wait yaklaşımıdır. Future tamamlanana kadar çağıranı bloklayan bir yöntem yoktur ve bu bir gözden kaçırma değildir. Ana iş parçacığından çağrılan bir Wait, bir arayüzü kilitlemenin klasik yoludur: işçi, yanıtını Synchronize ile ana iş parçacığa geçirmek için ana iş parçacığına ihtiyaç duyar; ana iş parçacığı Wait içinde park etmiştir ve iki taraf da ilerleyemez. Bu ilkelin sunulmaması, bu işi kendisi yazmayı deneyenlerin en çok devirdiği örüntüyü elemine eder. Gerçekten bloklaması gereken kod, sıradan bir TThread kullanıp sonuçlarına katlanmalıdır. Future, fire-and-forget durumu içindir; arka plan render de zaten tam olarak budur
Sonuç, TPdfFutureResult<T> içine sarılır; bu kayıt, yanına üç şeyden hangisinin gerçekleştiğini söyler. IsSuccess, işçinin normal döndüğünü ve Value'un render'i taşıdığını anlamına gelir. IsCancelled, belirtecin tetiklendiğini ve işçinin bir iptal noktasında çekildiğini anlamına gelir. IsFailure, işçinin istisna fırlattığını anlamına gelir ve ErrorMessage metni taşır. Yanıt, durumları bir kez inceler ve dallanır; bir sentinel değere bakarak dönen bitmap'in gerçek olup olmadığını tahmin etmez
Yanıt neden Queue ile değil Synchronize ile gider?
Bu birimdeki en öğretici kısım tek bir satırdır: yanıtın ana iş parçacığına nasıl döndüğü. Bariz tercih TThread.Queue'dur. Queue, yanıtı ana iş parçacığının kuyruğuna ekler ve anında döner; fire-and-forget bir future'un istediği şeye tam olarak benzer. Burada yanlıştır ve nedeni açıklamaya değer; çünkü bu, yazmayı düşünebileceğiniz bütün testleri geçen türden bir hatadır
İşçi iş parçacığı FreeOnTerminate := True ile oluşturulur. Bu, Execute döner dönmez iş parçacığının kendini söktüğü ve TThread.Destroy'un temizliğin bir parçası olarak RemoveQueuedEvents(Self)'i çağırdığı anlamına gelir. RemoveQueuedEvents, hedefi ölmekte olan iş parçacığı olan kuyruğa alınmış tüm metotları temizler. Dizi şöyle işler: işçi biter, yanıtı kendisine karşı kuyruğa ekler, Execute döner, iş parçacığı kendini yok eder ve RemoveQueuedEvents, ana iş parçacığın henüz çalıştırmadığı yanıtı siler. Sonuç tamamen kaybolur. Daha kötüsü, ana iş parçacık kuyruktaki yanıtı alıp çalıştırmaya başladığı an ile iş parçacığının serbest bırakıldığı an örtüştüğünde, yanıt yarısı yok edilmiş bir nesnenin alanlarına dokunur; bu bir use-after-free'dir
FPdfAsync bu yüzden yanıtı Queue ile değil Synchronize ile iletir. Synchronize, ana iş parçacık yanıtı sonuna kadar çalıştırana dek işçi iş parçacığını bloklar. Yanıt çalışırken işçi hâlâ yaşıyor; altından bir şey serbest bırakılacak yoktur ve iş parçacığı, yanıt iletilemeden Execute'tan dömez (dolayısıyla kendini yok etmeye başlamaz). Teslimat garantidir ve use-after-free penceresi yoktur
procedure TPdfFutureThread<T>.Execute;
begin
FResult.Status := pfsSuccess;
FResult.ErrorMessage := '';
try
FToken.ThrowIfCancelled; // zaten iptal edildi mi? çalışanı atla
FResult.Value := FWorker(FToken);
except
on E: EPdfOperationCancelled do
begin
FResult.Status := pfsCancelled;
FResult.ErrorMessage := E.Message;
end;
on E: Exception do
begin
FResult.Status := pfsFailure;
FResult.ErrorMessage := E.Message;
end;
end;
if Assigned(FReply) then
// Synchronize değil Queue: bu iş parçacığı FreeOnTerminate, bu yüzden kuyruğa alınmış bir yanıt
// ana iş parçacığı çalıştırmadan önce RemoveQueuedEvents tarafından düşürülebilir.
Synchronize(DispatchReply);
end;
Genel ders, tek bir birimin çok ötesine uzanır. Fire-and-forget eşzamansız geri çağrılar, ince biçimde yanlış yapması en kolay eşzamanlılık örüntüsüdür; çünkü mutlu yol ilk denemede çalışır ve hata, iş parçacığı söküm sırası ile kuyruk arasındaki etkileşimde yaşar. İstek üzerine yeniden üretilemez. Ana iş parçacığın kuyruğu boşaltmış olmasına ve işçinin kendini yok etmiş olmasına bağlıdır; zamanlayıcı bunu her koşuda farklı karar verir. Bağlayıcı katmanda bir kez doğru yapılmış bir ilkel, arka plan render gerektiren her uygulamada aynı kodu yeniden türetmekten çok daha değerlidir
Geri çağrılar neden metot işaretçileri?
İşçi ve yanıt anonim metot değildir. procedure of object türleridir — TPdfFutureWorker<T> ve TPdfFutureReply<T> — ve bu tercih derleyici matrisi tarafından dayatılmıştır. PDFiumPas, Delphi XE5 ve üzerinde ve Delphi modundaki Free Pascal 3.2 üzerinde derlenir; o modda FPC 3.2 anonim metotları desteklemez. Yerel değişkenleri yakalayan bir reference-to-procedure geri çağrı Delphi'de derlenir, FPC'de derlenmez; bu yüzden birim, iki derleyicinin de kabul ettiği en düşük ortak paydayı kullanır
Pratik sonuç, durumun nerede yaşadığıdır. Bir anonim metot yerelleri kapatır; bir metot işaretçisi kapatmaz. Bu yüzden işçinin ihtiyaç duyduğu her durum — sayfa indeksi, yakınlaştırma, çıktı yolu — ve yanıtın güncellemesi gereken her durum — hedef görüntü kontrolü veya ilerleme etiketi — metodu geçirilen nesneye asılmalıdır. Bir görüntüleyicide bu nesne genellikle form ya da formun sahiplendiği bir render denetleyicisidir. Bu, istemeye istemeye kabullenilen bir geçici çözüm değildir; durumun sahipliğini, kapanışın içine gizlemek yerine, alan nesne üzerinde açık ve görünür kılar
Sert sonlandırma değil, işbirlikçi iptal
Buradaki iptal işbirlikçidir. İşçi iş parçacığının içine girip onu sonlandıran bir API yoktur; çünkü render ortasında bir iş parçacığını sonlandırmak, PDFium'u kilitlerin ve yarısı yazılmış bitmaplerin başında bırakır ve zorla öldürmeden sonraki süreç durumu hakkında akıl yürütülebilir bir şey kalmaz. Onun yerine işçiye salt okunur bir belirteç verilir ve onu denetlemesi beklenir; render döngüsü de durmanın temiz olduğu yerlerde — sayfalar arasında ya da döşemeler arasında — denetlenecek şekilde yazılır
Belirteç, iptali gözlemenin üç yolunu sunar. IsCancelled, kendisi test edip karar vermek isteyen bir döngü için ucuz bir boolean yoklamasıdır. ThrowIfCancelled yaygın durumdur: doğal bir iptal noktasında çağırın, iptal istenmişse EPdfOperationCancelled fırlatır ve istisna işçiyi doğrudan future'a geri sarar. RegisterCallback, kaynak iptal edildiğinde bir kez tetiklenen tek atımlık bir bildirim ekler; işçinin sıkı bir döngüde değil, kesintiye uğratabileceği bir şeyin içinde bloke olduğu durumlarda kullanışlıdır
İş parçacığı sınırının önem taşıdığı yer istisnadır. İşçi EPdfOperationCancelled fırlattığında future bunu yakalar ve iptal edilmiş durumuna çevirir; yani yanıt bir başarısızlık değil IsCancelled görür. İstisna nesnesinin kendisi asla ana iş parçacığa taşınmaz. İşçi iş parçacığında doğar ve orada ölür; yalnızca mesaj dizisi ErrorMessage'a kopyalanır. Yaşayan bir istisna nesnesini iş parçacıkları arasında taşımak, işi bitmekte olan bir iş parçacığının sahiplendiği belleğe el atmak anlamına gelir; Synchronize ile teslimatın var olma nedeni de tam olarak bu sınıf hatayı önlemektir. Sınırı bir durum kodu ve bir dizi temiz geçer; bir nesne geçmez
İki arayüz: işçi kendini iptal edemesin
İptal, bilinçli olarak iki arayüze bölünmüştür. IPdfCancellationTokenSource yazma tarafıdır: Cancel içerir ve onu oluşturan sahibi — genellikle form — elinde tutar ve kullanıcı düğmeye bastığında ya da form kapandığında Cancel çağırır. IPdfCancellationToken okuma tarafıdır: IsCancelled, ThrowIfCancelled ve RegisterCallback içerir ve işçinin aldığı şey tam olarak budur. Tek bir somut nesne ikisini de uygular ama işçiye yalnızca belirteç verilir; yani çalıştırdığı işlemi iptal etmesinin bir yolu yoktur. Bu bölünme API düzeyinde bir koruma bariyeridir. Belirteci üzerinden Cancel'a ulaşabilen bir işçi, kafa karışmış bir kod parçasını kendini iptal etmeye davet ederdi; tip sistemi bu olasılığı ortadan kaldırır
Çağıranın bir render istediği ama asla iptal etmeyi amaçlamadığı durum için eşleşen bir ayrıntı vardır. Birim, her çağrı için taze bir kaynak zorlamak yerine kalıcı olarak iptal-edilmemiş durumda olan tekil bir belirteç olan PdfNoCancellationToken'ı açığa çıkarır. Run, belirteç argümanı nil bırakıldığında onu yerine koyar. Bu tekil, ilk kullanımda tembel olarak değil, birim başlatması sırasında baştan inşa edilir; nedeni yine eşzamanlılıktır. Farklı işçi iş parçacıklarındaki birkaç Run çağrısı aynı anda tembel oluşturulan bir tekile uzansaydı, inşasında yarışabilir, bir kopya sızdırabilir ya da yarısı başlatılmış bir örnek görebilirdi. Onu herhangi bir işçi çalışmadan önce inşa etmek, yarışı tümüyle ortadan kaldırır
İptal edilebilir bir render çalıştırmak
Pratikte bir kaynak oluşturur, onu formda tutar, Token'ını bir işçi metodu ve bir yanıt metoduyla birlikte Run'a geçersiniz ve İptal düğmesini kaynağa bağlarsınız. İşçi render ederken belirteci denetler; yanıt, sonuç döndüğünde arayüzü bir kez günceller. Geri çağrılar metot işaretçileri olduğundan, işçi ve yanıt formun alanlarından ihtiyaç duydukları her şeyi okur
procedure TMainForm.StartRender;
begin
FCancelSource := TPdfCancellationTokenSource.New; // alan, form üzerinde yaşar
TPdfFuture<Boolean>.Run(RenderWorker, RenderReply, FCancelSource.Token);
end;
procedure TMainForm.CancelButtonClick(Sender: TObject);
begin
if Assigned(FCancelSource) then
FCancelSource.Cancel; // çalışan bunu bir sonraki iptal noktasında gözlemler
end;
// Arka plan iş parçacığında çalışır. FPageRange / FOutputDir değerlerini formdan okur.
function TMainForm.RenderWorker(const AToken: IPdfCancellationToken): Boolean;
var
PageIndex: Integer;
begin
for PageIndex := FFirstPage to FLastPage do
begin
AToken.ThrowIfCancelled; // sayfalar arasında temiz duruş
RenderOnePage(PageIndex); // senkron PDFium rasterleştirmesi
end;
Result := True;
end;
// Ana iş parçacığında çalışır. Burada VCL'ye dokunmak güvenlidir.
procedure TMainForm.RenderReply(const AResult: TPdfFutureResult<Boolean>);
begin
if AResult.IsSuccess then
StatusLabel.Caption := 'Render complete'
else if AResult.IsCancelled then
StatusLabel.Caption := 'Cancelled'
else
StatusLabel.Caption := 'Failed: ' + AResult.ErrorMessage;
end;
Yanıt üç sonucu da ele alır; çünkü üçü de ulaşılabilirdir. Biten bir render başarı bildirir, İptal'e basan kullanıcı iptal edildi dalını görür ve yazılamayan bir dosya ya da ayrıştırılamayan bir sayfa, mesajlı bir başarısızlık olarak gelir. Bu dalların hiçbiri bloklamaz, hiçbiri işçi iş parçacığına dokunmaz ve işçinin ürettiği bitmap ya da durum, future tarafından UI'ye sahip iş parçacığında teslim edildikten sonra okunur
Aynı iş parçacığı disiplini bir görüntüleyicinin başka yerlerinde de karşılığını verir. Render edilen bitmaplerin yakınlaştırma değişimlerinde saklanması ve yeniden kullanılması render önbelleği ve yakınlaştırma performansı notumuzda, PDFium sınırını Delphi altında güvenli tutmanın daha geniş sorusu ise PDFium Component ABI'sini bellek güvenliği için sağlamlaştırma yazısında ele alınır. Burada anlatılan async altyapısı, Delphi ve C++Builder için PDFium Component'in parçası olarak, bu blogda başka yerde ele alınan rendering, metin ve form API'leriyle birlikte gelir