Upodabljanje strani v PDFium je sinhrono. Pokličete v knjižnico, ta rasterizira v bitno sliko, ki ste jo podali, in ko so piksli zapisani, se nadzor vrne. Za eno stran zaslonske velikosti pri eni ravni povečave to traja nekaj milisekund in nihče ne opazi. Za 300 dpi izvoz 200-stranskega dokumenta ali pasico z miniaturami, ki mora rasterizirati vsako stran naenkrat, isti klic stane sekunde. Če ta klic izvedete iz glavne niti, se zanka sporočil ustavi, okno preneha posodabljati in Windows napiše grozljivo "Ne odziva" čez naslovnico. Delo je pravilno. Kraj, kjer ste ga zagnali, je napačen
Popravek je premakniti dolgo upodabljanje v nit ozadja in prinesti rezultat nazaj v glavno nit, kjer je bitno sliko mogoče predati kontroli. PDFium vam tega ne preprečuje, a vezava mora narediti predajo varno, ker je površina napak okoli "zaženi na delavcu, odgovori na UI" velika in napake so občasne. Enota FPdfAsync v PDFiumPas daje temu vzorcu eno pravilno implementacijo z modelom preklica, ki ustreza načinu dejanskega delovanja dolgega upodabljanja
Oblika dela
Tri operacije prevladujejo v primerih, kjer upodabljanje preseže okvir. Paketno upodabljanje hodi skozi obseg strani in rasterizira vsako stran, ponavadi na disk. Vecstranski izvoz naredi enako, a sestavi izhod v eno datoteko. Upodabljanje strani v ozadju je tisto, kar pregledovalnik naredi, ko uporabnik skoči na stran, ki še ni v predpomnilniku, tako da je bitna slika ustvarjena izven niti in prikazana, ko je pripravljena. Vse tri imajo enake omejitve. Tečejo dovolj dolgo, da jih nit UI ne more gostiti, producirajo rezultat, ki ga nit UI sčasoma potrebuje, in uporabnik jih lahko zapusti. Zapiranje dokumenta, drsenje mimo strani ali pritisk Prekliči bi morali ustaviti delo namesto da silijo uporabnika v čakanje na izhod, ki ga ne želi vec
Ta zadnja omejitev je tista, ki oblikuje zasnovo. Upodabljanje, ki ga ni mogoče preklicati, je upodabljanje, ki drži dokument odprt in trosi CPU potem ko odgovor preneha biti pomemben. Zato je enota zgrajena okoli dveh primitiv, ki se sestavljata: prihodnosti, ki nosi rezultat nazaj, in žetona, ki nosi zahtevo za preklic naprej
Prihodnost brez čakanja
TPdfFuture<T>.Run sprejme delavca, odgovor in izbirni žeton za preklic. Zažene delavca v niti ozadja in ko delavec konča, dostavi odgovor v glavni niti. Generični parameter T je karkoli upodabljanje producira, pogosto ročaj bitne slike ali statusni zapis. Delavec tece izven niti; odgovor tece tam, kjer je varno dotikati se VCL
class procedure TPdfFuture<T>.Run(
const AWorker: TPdfFutureWorker<T>;
const AReply: TPdfFutureReply<T>;
const AToken: IPdfCancellationToken = nil); static;
Namerna izpustitev je vsaka vrsta Wait. Ni metode, ki bi blokirala klicatelja, dokler se prihodnost ne zaključi, in to ni spregledano. Wait, klican iz glavne niti, je klasičen način za zastoj UI: delavec potrebuje glavno nit, da zažene svoj odgovor prek Synchronize, glavna nit je parkirana znotraj Wait, in nobena stran ne more napredovati. Z zavrnitvijo ponujanja primitive prihodnost izključi vzorec, ki najpogosteje premaga tiste, ki skušajo to napisati sami. Koda, ki resnično mora blokirati, bi morala uporabiti navaden TThread in lastiti posledice. Prihodnost je za primer zastreli-in-pozabi, kar upodabljanje v ozadju dejansko je
Rezultat je zavit v TPdfFutureResult<T>, zapis, ki pove odgovoru, katera od treh stvari se je zgodila. IsSuccess pomeni, da je delavec vrnil normalno in Value drži upodabljanje. IsCancelled pomeni, da se je žeton sprožil in delavec se je umaknil na točki preklica. IsFailure pomeni, da je delavec dvignil, in ErrorMessage nosi besedilo. Odgovor enkrat preveri status in se razveji, namesto da ugibamo iz vrednosti stražnika, ali je vrnjena bitna slika resnična
Tekmovalni pogoj v1.61.0, ki je spremenil dostavo odgovora
Najpoukaznejsi del te enote je sprememba ene vrstice, ki je vzela čas za razumevanje. Skozi zgodnje različice je nit delavca dostavila odgovor s TThread.Queue. Queue objavi odgovor v čakalno vrsto glavne niti in se takoj vrne, kar se bere kot točno tisto, kar želi prihodnost zastreli-in-pozabi. Bilo je napačno, in razlog je vreden razlage, ker je to vrsta napake, ki prestane vsak test, ki si ga zamislite
Nit delavca je ustvarjena z FreeOnTerminate := True. To pomeni, da v trenutku, ko se Execute vrne, se nit sama poruši, in TThread.Destroy pokliče RemoveQueuedEvents(Self) kot del čiščenja. RemoveQueuedEvents počisti vse metode v čakalni vrsti, katerih cilj je umirajoča nit. Torej je bila sekvenca: delavec konča, objavi odgovor v svojo čakalno vrsto, Execute se vrne, nit se sama uniči, in RemoveQueuedEvents izbriše odgovor, ki ga glavna nit se ni zagnala. Rezultat je preprosto izginil. Se slabse, v ozkem oknu, kjer je glavna nit potegnila odgovor iz čakalne vrste in ga začela zaganjati hkrati, ko je nit bila sproščena, je odgovor dotaknil polja napol uničenega objekta, kar je uporaba po sprostitvi
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;
Splošna lekcija preseže specifičen popravek. Asinhronski povratni klici zastreli-in-pozabi so najlajsi vzorec sočasnosti za subtilno narobe, ker srečna pot deluje pri prvem poskusu in napaka živi v interakciji med vrstnim redom uničevanja niti in čakalno vrsto. Ne reproducira se na zahtevo. Odvisno je od tega, ali je glavna nit izpraznila čakalno vrsto preden je delavec slučajno končal z uničevanjem samega sebe, kar je usklajevanje, ki ga razporejevalnik odloča drugače pri vsakem zagonu. Primitivna, ki je pravilna enkrat, v vezavi, je vredna daleč več kot enaka koda, ki je na novo izvedena v vsaki aplikaciji, ki potrebuje upodabljanje v ozadju
Zakaj so povratni klici kazalci na metode
Delavec in odgovor nista anonimni metodi. Sta tipa procedure of object, TPdfFutureWorker<T> in TPdfFutureReply<T>, in ta izbira je prisilna s strani matrike prevajalnikov. PDFiumPas prevede na Delphi XE5 in kasnejsih ter na Free Pascal 3.2 v Delphi načinu, in FPC 3.2 v tem načinu ne podpira anonimnih metod. Povratni klic, ki nanaša reference na procedure in zajame lokalne spremenljivke, bi se prevedel na Delphi in ne uspel na FPC, zato enota uporablja najnižji skupni imenovalec, ki ga sprejmeta oba prevajalnika
Praktična posledica je, kje živi stanje. Anonimna metoda zapre nad lokalnimi spremenljivkami; kazalec na metodo ne. Torej mora vsako stanje, ki ga delavec potrebuje -- indeks strani, povečava, izhodna pot -- in vsako stanje, ki ga odgovor potrebuje za posodobitev -- ciljna slikovna kontrola ali oznaka napredka -- viseti na objektu, katerega metoda se posreduje. V pregledovalniku je ta objekt ponavadi obrazec ali krmilnik upodabljanja, ki ga poseduje. To ni rešitev, vsiljena s sklenjenostjo; ohranja lastništvo tega stanja eksplicitno in vidno na sprejemnem objektu namesto skrito znotraj zaprtja
Kooperativna prekinitev, ne trda zaustavitev
Preklic je tukaj kooperativen. Ni API-ja, ki bi dosegel v delavsko nit in jo prekinil, ker prekinjanje niti sredi upodabljanja pusti PDFium z zaklenjenimi in delno zapisanimi bitnimi slikami, in stanje procesa po prisilni zaustavitvi ni nekaj, o čemer bi se dalo razumno razmišljati. Namesto tega je delavec podan bralno-samo žeton in se od njega pričakuje, da ga preveri, in zanka upodabljanja je napisana tako, da jo preveri med stranmi ali med ploščicami, kjer je ustavitev čista
Žeton ponuja tri načine za opazovanje preklica. IsCancelled je poceni logično preverjanje za zanko, ki želi testirati in se odločiti sama. ThrowIfCancelled je pogost primer: pokliči ga na naravni točki preklica in, če je bil preklic zahtevan, dvigne EPdfOperationCancelled, kar odmota delavca naravnost nazaj v prihodnost. RegisterCallback priloži enostreljno obvestilo, ki se sproži enkrat, ko je vir preklican, uporabno ko je delavec blokiran v nečem, kar lahko prekine, namesto da sedi v tesni zanki
Izjema je tam, kjer meja niti steje. Ko delavec dvigne EPdfOperationCancelled, prihodnost to ujame in pretvori v preklicani status, tako da odgovor vidi IsCancelled in ne napako. Sam objekt izjeme nikoli ni prenesen v glavno nit. Živi in umre na delavski niti; samo njen niz sporočila je kopiran v ErrorMessage. Prenos živega objekta izjeme med nitmi bi pomenil doseganje v spomin v lasti niti, ki se zaključuje, kar je enaka razred napake, ki jo popravek Synchronize obstaja za preprečevanje. Statusna koda in niz prečkata mejo čisto; objekt ne bi
Dve vmesnici, da delavec ne more preklicati sam sebe
Preklic je namerno razdeljen na dve vmesnici. IPdfCancellationTokenSource je stran pisanja: ima Cancel, in lastnik, ki ga ustvari, ponavadi obrazec, ga obdrži in pokliče Cancel, ko uporabnik klikne gumb ali se obrazec zapre. IPdfCancellationToken je stran branja: ima IsCancelled, ThrowIfCancelled in RegisterCallback, in to je vse, kar delavec kdaj prejme. En konkreten objekt implementira oba, a delavec dobi le žeton, tako da nima načina za preklic operacije, ki jo izvaja. Ločitev je varnostna ograja na ravni API. Delavec, ki bi lahko dosegel Cancel prek svojega žetona, bi povabil zmeden del kode, da se sam prekliče, in sistem tipov odstrani to možnost
Obstaja ujemajoča se podrobnost za primer, kjer klicatelj želi upodabljanje, a ga nikoli ne namerava preklicati. Namesto da bi silili svež vir na vsak klic, enota izpostavi PdfNoCancellationToken, singleton žeton, ki je trajno v stanju ni-preklican. Run ga nadomesti, ko je argument žetona pusti nil. Ta singleton je zgrajen navdusenostno med inicializacijo enote namesto leničarsko ob prvi uporabi, in razlog je spet sočasnost. Če bi več klicev Run na različnih delavskih nitih hkrati doseglo leničarsko ustvarjen singleton, bi se lahko tekmovale pri njegovi gradnji, pustile podvajanje ali kratko opazovale napol inicializirano instanco. Gradnja pred zagonom katerega koli delavca v celoti odstrani tekmovalni pogoj
Zagon odpovedljivega upodabljanja
V praksi ustvarite vir, ga hranite na obrazcu, posredujete njegov Token v Run skupaj z metodo delavca in metodo odgovora, in povežete gumb Prekliči z virom. Delavec med upodabljanjem preverja žeton; odgovor posodobi UI, ko je rezultat nazaj. Ker so povratni klici kazalci na metode, delavec in odgovor berejo, kar potrebujeta, iz polj obrazca
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;
Odgovor obravnava vse tri izide, ker so vsi trije dosegljivi. Dokončano upodabljanje poroča uspeh, uporabnik, ki je pritisnil Prekliči, vidi preklicano vejo, in datoteka, ki je ni bilo mogoče zapisati, ali stran, ki ni bila razčlenjena, prispe kot napaka s sporočilom. Nobena od teh vej ne blokira, nobena se ne dotika delavske niti, in bitna slika ali status, ki ga je produciral delavec, se prebere le po tem, ko je prihodnost dostavila na niti, ki poseduje UI
Enaka disciplina niti se obrestuje drugje v pregledovalniku. Način, kako so upodobljene bitne slike hranjene in ponovno uporabljene med spremembe povečave, je opisan v našem zapisu o predpomnilniku upodabljanja in zmogljivosti povečave, in širše vprašanje ohranjanja varnosti meje PDFium pod Delphijem je v utrjevanju PDFium VCL ABI za varnost pomnilnika. Asinhronska infrastruktura, opisana tukaj, se dobavlja kot del Komponente PDFium za Delphi in C++Builder, skupaj z upodabljanjem, besedilom in API-ji obrazcev, opisanimi drugje na tem blogu