PDFium'da bir sayfanın oluşturulması senkrondur (synchronous). Kitaplığa çağrı yaparsınız, ona verdiğiniz (handed) bir bitmap (bitmap) içine rasterleştirir (rasterises) ve pikseller (pixels) yazıldığında kontrol geri döner. Tek bir yakınlaştırma seviyesindeki tek bir ekran boyutundaki sayfa için bu birkaç milisaniye (milliseconds) sürer ve kimse fark etmez. 200 sayfalık bir belgenin (document) 300 dpi dışa aktarımı veya her sayfayı aynı anda rasterleştirmesi gereken bir küçük resim (thumbnail) şeridi için aynı çağrı saniyelere (seconds) mal olur. Bu çağrıyı ana iş parçacığından (main thread) yaparsanız, ileti döngüsü (message loop) durur, pencere yeniden boyamayı (repainting) bırakır ve Windows, başlık çubuğunuzun üzerine o korkulan "Yanıt Vermiyor" (Not Responding) yazısını çizer (paints). İş doğrudur. Onu çalıştırdığınız (ran) yer yanlıştır
Düzeltme (fix), uzun süren oluşturmayı arka plan iş parçacığına (background thread) taşımak ve sonucu (result), bitmap'in bir denetime (control) devredilebileceği ana iş parçacığına geri getirmektir. PDFium'un kendisi bunu yapmanıza engel olmaz, ancak bağlamanın (binding) geçişi (handoff) güvenli (safe) hale getirmesi gerekir, çünkü "bir çalışanda (worker) çalıştır, kullanıcı arayüzünde yanıtla" (run on a worker, reply on the UI) etrafındaki hata (bug) yüzeyi geniştir ve arızalar (failures) aralıklıdır (intermittent). PDFiumPas'daki FPdfAsync birimi (unit), uzun bir oluşturmanın (render) gerçekte (actually) nasıl davrandığına (behaves) uyan (fits) bir iptal etme (cancellation) modeliyle (model) bu kalıba (pattern) tek bir doğru (correct) uygulama (implementation) sağlamak (give) için vardır
İşin şekli
Bir oluşturmanın (render) bir kareden (frame) daha uzun sürdüğü durumlara (cases) üç operasyon hakimdir (dominate). Toplu oluşturma (Batch rendering), bir sayfa aralığında gezinir ve her sayfayı genellikle diske rasterleştirir. Çok sayfalı (Multi-page) dışa aktarım da aynısını yapar ancak çıktıyı (output) tek bir dosyada birleştirir (assembles). Arka plan sayfa oluşturma (Background page rendering), kullanıcı henüz önbellekte (cache) olmayan bir sayfaya atladığında bir görüntüleyicinin (viewer) yaptığı şeydir, dolayısıyla bitmap iş parçacığı (thread) dışında (off-thread) üretilir ve hazır olduğunda (ready) gösterilir. Üçü de aynı (same) kısıtlamaları (constraints) paylaşır. UI iş parçacığının barındıramayacağı (host) kadar uzun süre (long enough) çalışırlar, UI iş parçacığının eninde sonunda ihtiyaç duyduğu bir sonuç üretirler ve kullanıcı onlardan vazgeçebilir (abandon). Belgeyi kapatmak, sayfayı kaydırarak (scrolling) geçmek veya İptal (Cancel) düğmesine basmak, kullanıcıyı artık (no longer) istemediği bir çıktıyı (output) beklemeye zorlamak yerine işi (work) durdurmalıdır
Bu son kısıtlama (constraint) tasarımı (design) şekillendiren şeydir (shapes). İptal edilemeyen (cannot be cancelled) bir oluşturma (render), belgeyi (document) açık (open) tutan ve yanıtın (answer) artık (stopped) bir önemi kalmadığında (mattering) bile CPU'yu (CPU) tüketen (burns) bir oluşturmadır. Bu nedenle birim (unit), birbirini tamamlayan (compose) iki temel (primitives) yapı (primitive) etrafında oluşturulmuştur: sonucu geriye taşıyan bir gelecek (future) ve iptal talebini (cancellation request) ileriye taşıyan bir belirteç (token)
Unut gitsin (fire-and-forget) türü bir gelecek (future)
TPdfFuture<T>.Run bir çalışan (worker), bir yanıt (reply) ve isteğe bağlı bir iptal belirteci (cancellation token) alır. Çalışanı arka planda bir iş parçacığında başlatır ve çalışan bittiğinde yanıtı ana iş parçacığına (main thread) iletir. Genel (generic) parametre T, oluşturmanın ürettiği her neyse (whatever) odur, genellikle bir bitmap tutamacı (handle) veya bir durum kaydıdır (status record). Çalışan iş parçacığı dışında (off-thread) çalışır; yanıt VCL'ye (VCL) dokunmanın güvenli olduğu (safe) yerde (where) çalışır
class procedure TPdfFuture<T>.Run(
const AWorker: TPdfFutureWorker<T>;
const AReply: TPdfFutureReply<T>;
const AToken: IPdfCancellationToken = nil); static;
Kasıtlı eksiklik (deliberate omission), herhangi bir tür (kind) Wait (Bekle) yöntemidir (method). Arayanı (caller) gelecek (future) tamamlanana kadar bloke edecek hiçbir yöntem yoktur ve bu bir gözden kaçırma (oversight) değildir. Ana iş parçacığından (main thread) çağrılan bir Wait, bir kullanıcı arayüzünü (UI) kilitlenmeye (deadlock) sokmanın klasik yoludur: çalışanın yanıtını Synchronize üzerinden geçirmek için ana iş parçacığına ihtiyacı vardır, ana iş parçacığı Wait içinde park halindedir ve her iki taraf da ilerleyemez. Bu temeli sunmayı reddederek, gelecek (future), bunu kendi başlarına yazmaya çalışan insanları en sık mağlup eden kalıbı (pattern) ortadan kaldırır. Gerçekten bloke olması gereken kod düz (plain) bir TThread kullanmalı ve sonuçlarına sahip çıkmalıdır. Gelecek (future), arka plan oluşturmanın gerçekte olduğu gibi, ateşle-ve-unut (fire-and-forget) durumları içindir
Sonuç, yanıta üç şeyden hangisinin gerçekleştiğini anlatan bir kayıt (record) olan TPdfFutureResult<T> içine sarılır (wrapped). IsSuccess çalışanın normal olarak döndüğü anlamına gelir ve Value oluşturmayı tutar (holds). IsCancelled belirtecin ateşlendiği (fired) ve çalışanın bir iptal noktasında (cancellation point) işi bıraktığı (bailed out) anlamına gelir. IsFailure çalışanın hata (raised) verdiği anlamına gelir ve ErrorMessage metni (text) taşır (carries). Yanıt (reply), geri döndürülen bir bitmap'in (bitmap) gerçek (real) olup olmadığını nöbetçi bir değerden (sentinel value) tahmin (guessing) etmek yerine (instead of) durumu (status) bir kez (once) inceler (inspects) ve dallanır (branches)
Yanıt teslimini (reply delivery) değiştiren v1.61.0 yarışı (race)
Bu birimin (unit) en öğretici (instructive) kısmı (part), anlaşılması biraz zaman alan (took a while) tek satırlık bir değişikliktir. Erken sürümler (early versions) boyunca (Through) çalışan iş parçacığı (worker thread) yanıtını (reply) TThread.Queue ile teslim etti (delivered). Kuyruk (Queue), yanıtı ana iş parçacığının (main thread's) kuyruğuna (queue) gönderir (posts) ve anında geri döner (returns immediately), bu da tam olarak bir fırlat ve unut (fire-and-forget) geleceğinin (future) isteyeceği şeymiş (exactly what) gibi okunur (reads like). Yanlıştı (It was wrong) ve nedenini hecelemeye (spelling out) değer çünkü yazmayı düşündüğünüz her testi geçen türden (kind) bir hatadır (bug)
Çalışan iş parçacığı (worker thread) FreeOnTerminate := True ile oluşturulur (created). Bu, Execute döndüğü anda iş parçacığının (thread) kendini (itself) yıkması (tears down) ve TThread.Destroy işlevinin (Destroy) temizliğin (cleanup) bir parçası olarak RemoveQueuedEvents(Self) işlevini (calls) çağırması anlamına gelir. RemoveQueuedEvents, hedefi ölmekte olan iş parçacığı olan sıraya alınmış tüm yöntemleri (queued method) temizler. Yani sıra şuydu (So the sequence was): çalışan biter, yanıtı (reply) kendisine karşı kuyruğa alır (queues), Execute döner (returns), iş parçacığı kendini yok eder ve RemoveQueuedEvents, ana iş parçacığının henüz (had not) çalıştırmadığı (run yet) yanıtı (reply) siler (deletes). Sonuç basitçe (simply) ortadan kayboldu (vanished). Daha da kötüsü (Worse), ana iş parçacığının sıradaki (queued) yanıtı alıp çalışmaya başladığı (started running it) ve tam (exactly) o sırada iş parçacığının (thread) serbest bırakıldığı o dar (narrow) pencerede (window), yanıt (reply) yarı yok edilmiş (half-destroyed) bir nesnenin alanlarına dokundu, bu da (which is) kurtulduktan sonra kullanımdır (use-after-free)
v1.61.0'daki düzeltme (fix), yanıtı (reply) Queue (Queue) yerine Synchronize (Synchronize) ile teslim (deliver) etmekti. Synchronize, ana iş parçacığı (main thread) yanıtı tamamlayana (run the reply to completion) kadar çalışan iş parçacığını (worker thread) engeller (blocks). Yanıtı (reply) çalıştırılırken (executes) çalışan hala hayattadır (alive), bu nedenle (so) altından kurtarılacak hiçbir şey (nothing) yoktur ve iş parçacığı (thread) yanıt iletilene (delivered) kadar Execute'den geri dönmez (ve dolayısıyla (therefore) kendi kendini yok etmeye (destroying itself) başlamaz). Teslimat garantilidir (guaranteed) ve serbest bırakıldıktan sonra (use-after-free) kullanımı penceresi kapanır (closed)
procedure TPdfFutureThread<T>.Execute;
begin
FResult.Status := pfsSuccess;
FResult.ErrorMessage := '';
try
FToken.ThrowIfCancelled; // already cancelled? skip the worker
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, not Queue: this thread is FreeOnTerminate, so a queued reply
// could be dropped by RemoveQueuedEvents before the main thread ran it.
Synchronize(DispatchReply);
end;
Genel (general) ders (lesson) belirli (specific) düzeltmeden (fix) daha uzun ömürlüdür (outlasts). Ateşle ve unut (Fire-and-forget) asenkron geri çağrılar (callbacks), hafifçe yanlış anlaması (get subtly wrong) en kolay eşzamanlılık (concurrency) kalıbıdır (pattern), çünkü mutlu yol (happy path) ilk denemede (try) çalışır (works) ve hata (bug) iş parçacığının sökülme (thread teardown) sırası (order) ile kuyruk (queue) arasındaki etkileşimde (interaction) yaşar. Talep üzerine çoğalmaz (It does not reproduce on demand). Bu, zamanlayıcının her çalıştırmada farklı (differently) şekilde karar verdiği bir zamanlama (timing) olan, ana iş parçacığının (main thread) çalışanın (worker) kendisini yok etmeyi bitirmeden önce kuyruğu (queue) boşaltıp (drain) boşaltmadığına bağlıdır (depends on). Bağlama (binding) içinde bir kez doğru (correct) olan (is) bir temel (primitive), arka plan (background) oluşturma (render) gerektiren her uygulamada (application) yeniden türetilen (re-derived) aynı koddan (code) çok daha değerlidir (worth)
Geri çağrılar (callbacks) neden yöntem işaretçileridir (method pointers)
Çalışan (worker) ve yanıt (reply) anonim yöntemler (anonymous methods) değildir. Onlar TPdfFutureWorker<T> ve TPdfFutureReply<T> gibi procedure of object türleridir ve bu seçim derleyici matrisi (compiler matrix) tarafından zorlanır (forced). PDFiumPas Delphi XE5 ve sonrasında ve Delphi modunda Free Pascal 3.2'de derlenir ve o moddaki FPC 3.2 anonim yöntemleri desteklemez (does not support). Yerel değişkenleri (local variables) yakalayan bir procedure referansı geri çağrısı (callback) Delphi'de derlenir (compile) ve FPC'de başarısız olurdu (fail), bu yüzden birim (unit) her iki derleyicinin (compilers) de kabul ettiği (accept) en düşük (lowest) ortak paydayı (common denominator) kullanır
Pratik sonuç (practical consequence) durumun (state) nerede yaşadığıdır. Anonim bir yöntem, yerel (locals) öğelerin üzerini kapatır (closes over); yöntem işaretçisi (method pointer) bunu yapmaz. Böylece çalışanın ihtiyaç duyduğu (needs) her durum (state), sayfa dizini (page index), yakınlaştırma (zoom), çıkış yolu (output path) ve yanıtın güncellemesi gereken (needs to update) her durum (state), hedef (target) görüntü kontrolü (image control) veya ilerleme etiketi (progress label), yöntemi geçirilmekte olan nesneye (object) asılmak zorundadır (has to hang off). Bir görüntüleyicide (viewer) o nesne genellikle form veya sahip olduğu bir oluşturma denetleyicisidir (render controller). Bu (This) gönülsüzce (grudgingly) dayatılan (imposed) geçici bir çözüm (workaround) değildir; bir kapatma (closure) içinde (inside) gizlenmek (hidden) yerine bu durumun sahipliğini (ownership) alıcı (receiving) nesne (object) üzerinde açık (explicit) ve görünür (visible) tutar (keeps)
Zorlu (hard) bir öldürme değil (not a hard kill), işbirlikçi iptal (Cooperative cancellation)
İptal (Cancellation) işlemi burada (here) işbirlikçidir (cooperative). Çalışan (worker) iş parçacığına uzanan (reaches into) ve onu sonlandıran (terminates it) hiçbir API yoktur, çünkü iş parçacığını bir oluşturma işleminin ortasında (mid-render) sonlandırmak PDFium'u kilitleri (locks) tutarken ve kısmen (partially) yazılmış bitmap'ler (bitmaps) halinde bırakır ve zorunlu bir öldürmeden (forced kill) sonra süreç durumu (process state) hakkında fikir yürütebileceğiniz bir şey değildir. Bunun yerine çalışana salt okunur (read-only) bir belirteç (token) teslim edilir ve bunu denetlemesi beklenir (expected) ve oluşturma döngüsü (render loop) durmanın temiz olduğu durumlarda (clean) bunu sayfalar arasında (between pages) veya döşemeler (tiles) arasında (between tiles) denetlemek üzere yazılır
Belirteç iptali (cancellation) gözlemlemek için üç yol (ways) sunar. IsCancelled kendi (itself) karar vermek (decide) ve test (test) etmek isteyen (wants to) bir döngü için (for a loop) ucuz (cheap) bir boole anketidir (boolean poll). ThrowIfCancelled (ThrowIfCancelled) yaygın durumdur (common case): bunu doğal bir iptal noktasında çağırın (call it at a natural cancellation point) ve iptal etme talep edildiyse, EPdfOperationCancelled uyarısı (raises) verir, bu da çalışanı (worker) doğruca (straight) geleceğe (future) geri döndürür (unwinds). RegisterCallback, kaynak iptal edildiğinde bir kez ateşlenen tek seferlik (one-shot) bir bildirim ekler; bir çalışan (worker) sıkı bir döngüde oturmak yerine (rather than sitting in a tight loop) kesebileceği (interrupt) bir şeyde bloke edildiğinde faydalıdır
İstisna, iş parçacığı sınırının (thread boundary) önemli olduğu yerdir. Çalışan (worker) EPdfOperationCancelled yükselttiğinde (raises), gelecek (future) onu yakalar (catches) ve onu iptal edilmiş bir duruma (cancelled status) dönüştürür (turns), böylece yanıt (reply) bir başarısızlık (failure) değil IsCancelled durumunu (IsCancelled) görür (sees). İstisna (exception) nesnesi (object) asla (never) ana iş parçacığına (main thread) sıralanmaz (marshaled). Çalışan (worker) iş parçacığında yaşar ve ölür (lives and dies); yalnızca (only) mesaj (message) dizesi (string) ErrorMessage içine kopyalanır (copied). Canlı (live) bir istisna (exception) nesnesini (object) iş parçacıkları (threads) arasında (across) sıralamak (Marshaling), bitmek (finishing) üzere olan (that is) bir iş parçacığının sahip olduğu (owned by) belleğe (memory) ulaşmak (reaching into) anlamına gelir; bu (which is), Synchronize düzeltmesinin (fix) engellemek (prevent) için (to) var olduğu hata (mistake) sınıfıyla (class) aynıdır (same). Bir durum kodu (status code) ve bir dize (string), sınırı (boundary) temiz bir şekilde (cleanly) geçer (cross); bir nesne (object) bunu yapmazdı (would not)
İki arayüz (Two interfaces), böylece bir çalışan kendi kendini iptal edemez
İptal (Cancellation) kasıtlı olarak (on purpose) iki arayüze (interfaces) bölünmüştür (split across). IPdfCancellationTokenSource (IPdfCancellationTokenSource) yazma (write) tarafıdır (side): Cancel'ı (Cancel) vardır (has) ve onu oluşturan (creates) sahip (owner) (genellikle (usually) form) onu tutar (keeps) ve kullanıcı düğmeye (button) tıkladığında (clicks) veya form kapandığında (closes) Cancel'ı (Cancel) çağırır. IPdfCancellationToken, okuma (read) tarafıdır (side): IsCancelled, ThrowIfCancelled ve RegisterCallback özelliklerine sahiptir (has) ve tüm çalışanların aldığı (all the worker ever receives) tek şey budur. Somut bir nesne (concrete object) her ikisini de (both) uygular (implements), ancak çalışana (worker) yalnızca belirteç (token) verilir, bu nedenle (so) çalıştırdığı (it is running) işlemi (operation) iptal etmesinin bir yolu yoktur (has no way to). Bölünme (split) API düzeyinde (API-level) bir koruma parmaklığıdır (guard rail). Belirteci (token) aracılığıyla Cancel'a ulaşabilen (reach) bir çalışan (worker), kendi kendini iptal etmesi (cancel itself) için karışık bir kod parçasını davet eder (invite) ve tür sistemi (type system) olasılığı ortadan kaldırır (removes)
Bir çağıranın (caller) bir oluşturma (render) istediği (wants) ancak bunu (it) iptal (cancel) etmeyi hiç amaçlamadığı (never intends to) durum (case) için eşleşen (matching) bir ayrıntı (detail) vardır. Çağrı (call) başına taze (fresh) bir kaynağı (source) zorlamak (force) yerine (Rather than), birim (unit), kalıcı olarak iptal-edilmemiş (not-cancelled) durumda olan tekton (singleton) bir belirteç (token) olan PdfNoCancellationToken değerini sunar (exposes). Belirteç (token) bağımsız değişkeni (argument) nil bırakıldığında Run bunun (it) yerine geçer (substitutes). Bu singleton, ilk kullanımda (first use) tembelce (lazily) değil (rather than), birim başlatma sırasında (during unit initialization) hevesle (eagerly) inşa edilir (constructed) ve nedeni yine eşzamanlılıktır (concurrency). Birden çok Run çağrısı farklı çalışan iş parçacıklarında aynı anda tembelce (lazily) yaratılmış (created) bir tekton nesneye (singleton) ulaşırsa (reached for), yapısında yarışabilir (race), bir kopyayı sızdırabilir (leak) veya kısaca yarı başlatılmış (half-initialised) bir örneği (instance) gözlemleyebilirdi (observe). Bunu, herhangi bir çalışan çalışabilmeden (run) önce oluşturmak, yarışı (race) tamamen (entirely) ortadan kaldırır (removes)
İptal edilebilir (cancellable) bir oluşturmayı (render) çalıştırma
Pratikte bir kaynak yaratır, onu formda tutar, bir çalışan metodu (worker method) ve bir yanıt metodu (reply method) ile birlikte Token değerini Run'a geçirirsiniz ve İptal (Cancel) butonunu kaynağa bağlarsınız (wire). Çalışan (worker) oluştururken (renders) belirteci kontrol eder; yanıt (reply), sonuç (result) geri döndüğünde (back) kullanıcı arayüzünü günceller (updates). Geri çağrılar yöntem işaretçisi (method pointers) oldukları için çalışan ve yanıt formun alanlarından neye ihtiyaç duyuyorlarsa onu okurlar
procedure TMainForm.StartRender;
begin
FCancelSource := TPdfCancellationTokenSource.New; // field, lives on the form
TPdfFuture<Boolean>.Run(RenderWorker, RenderReply, FCancelSource.Token);
end;
procedure TMainForm.CancelButtonClick(Sender: TObject);
begin
if Assigned(FCancelSource) then
FCancelSource.Cancel; // worker observes this at its next cancel point
end;
// Runs on a background thread. Reads FPageRange / FOutputDir from the form.
function TMainForm.RenderWorker(const AToken: IPdfCancellationToken): Boolean;
var
PageIndex: Integer;
begin
for PageIndex := FFirstPage to FLastPage do
begin
AToken.ThrowIfCancelled; // clean stop between pages
RenderOnePage(PageIndex); // synchronous PDFium rasterisation
end;
Result := True;
end;
// Runs on the main thread. Safe to touch the VCL here.
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 (reply) üç (three) sonuca (outcomes) da müdahale eder (handles) çünkü üçüne de ulaşılabilir (reachable). Tamamlanan (finished) bir oluşturma başarıyı (success) bildirir (reports), İptal'e basan (pressed Cancel) bir kullanıcı iptal edilen (cancelled) dalı görür (sees) ve yazılamayan (could not be written) bir dosya veya ayrıştırılamayan (failed to parse) bir sayfa, bir mesajla (message) birlikte (with) bir arıza (failure) olarak ulaşır (arrives as). Bu dalların hiçbiri engelleme yapmaz (block), hiçbiri (none of them) çalışan (worker) iş parçacığına (thread) dokunmaz (touch) ve çalışanın ürettiği (produced) bitmap (bitmap) veya durum (status) ancak (only) gelecek (future) onu kullanıcı arayüzüne (UI) sahip olan (owns) iş parçacığına (thread) teslim (delivered) ettikten sonra okunur (read)
Aynı iş parçacığı (threading) disiplini bir görüntüleyicinin (viewer) başka yerlerinde (elsewhere) de işe yarar (pays off). İşlenen (rendered) bit eşlemlerin (bitmaps) nasıl tutulduğu (kept) ve yakınlaştırma (zoom) değişiklikleri (changes) boyunca (across) nasıl yeniden kullanıldığı (reused) oluşturma önbelleği ve yakınlaştırma performansına ilişkin notumuzda ele alınmıştır (covered in) ve (and) Delphi altında PDFium sınırını güvenli tutmanın daha geniş (broader) sorusu bellek güvenliği için PDFium VCL ABI'sini sağlamlaştırma bölümündedir. Burada (here) açıklanan (described) asenkron altyapı (async infrastructure), bu blogun başka yerlerinde (elsewhere on this blog) ele alınan oluşturma, metin ve form API'lerinin yanı sıra, Delphi ve C++Builder için PDFium Bileşeninin bir parçası olarak gönderilir (ships)