Gengivelse af en side i PDFium er synkron. Du kalder ind i biblioteket, det rasteriserer til et bitmap, du har givet det, og kontrollen vender tilbage, når pixlerne er skrevet. For en enkelt skærmstørrelse-side ved ét zoomniveau tager det nogle få millisekunder, og ingen lægger mærke til det. For en 300 dpi-eksport af et 200-siders dokument, eller en miniaturestribe, der skal rasterisere hver side på én gang, koster det samme kald sekunder. Hvis du foretager det kald fra hovedtråden, stopper meddelelsessløjfen, vinduet holder op med at tegne igen, og Windows maler det frygtede "Svarer ikke" over din titellinje. Arbejdet er korrekt. Det sted, du kørte det, er forkert
Løsningen er at flytte den lange gengivelse over på en baggrundstråd og bringe resultatet tilbage til hovedtråden, hvor bitmappet kan overdrages til en kontrol. PDFium selv forhindrer dig ikke i at gøre dette, men bindingen er nødt til at gøre overdragelsen sikker, fordi fejlfladen omkring "kør på en arbejder, svar på UI'en" er bred, og fejlene er periodiske. Enheden FPdfAsync i PDFiumPas eksisterer for at give dette mønster én korrekt implementering, med en annulleringsmodel, der passer til, hvordan en lang gengivelse faktisk opfører sig
Arbejdets form
Tre operationer dominerer de tilfælde, hvor en gengivelse overlever en frame. Batch-gengivelse gennemløber et sideinterval og rasteriserer hver side, normalt til disk. Flersidet eksport gør det samme, men samler outputtet i én fil. Baggrundssidegengivelse er, hvad en fremviser gør, når brugeren springer til en side, der endnu ikke er i cachen, så bitmappet produceres af-tråd og vises, når det er klar. Alle tre deler de samme begrænsninger. De kører længe nok til, at UI-tråden ikke kan være vært for dem, de producerer et resultat, som UI-tråden i sidste ende har brug for, og brugeren kan opgive dem. At lukke dokumentet, scrolle forbi siden eller trykke på Annuller bør stoppe arbejdet i stedet for at tvinge brugeren til at vente på output, de ikke længere ønsker
Den sidste begrænsning er den, der former designet. En gengivelse, der ikke kan annulleres, er en gengivelse, der holder dokumentet åbent og brænder CPU af, efter at svaret er holdt op med at betyde noget. Så enheden er bygget op omkring to primitiver, der sammensættes: en future, der bærer resultatet tilbage, og et token, der bærer annulleringsanmodningen fremad
En affyr-og-glem-future
TPdfFuture<T>.Run tager en arbejder, et svar og et valgfrit annulleringstoken. Den starter arbejderen på en baggrundstråd, og når arbejderen er færdig, leverer den svaret på hovedtråden. Den generiske parameter T er, hvad end gengivelsen producerer, ofte et bitmap-håndtag eller en statusrecord. Arbejderen kører af-tråd; svaret kører der, hvor det er sikkert at røre ved VCL'en
Den bevidste udeladelse er enhver form for Wait. Der er ingen metode til at blokere kaldet, indtil futuren fuldføres, og det er ikke en forglemmelse. En Wait kaldt fra hovedtråden er den klassiske måde at fastlåse en UI på: arbejderen har brug for hovedtråden til at køre sit svar gennem Synchronize, hovedtråden er parkeret inde i Wait, og ingen af siderne kan fortsætte. Ved at nægte at tilbyde primitiven udelukker futuren det mønster, der oftest besejrer folk, som forsøger at skrive dette selv. Kode, der ægte har brug for at blokere, bør bruge en almindelig TThread og tage konsekvenserne. Futuren er til affyr-og-glem-tilfældet, hvilket er, hvad baggrundsgengivelse faktisk er
Resultatet er pakket ind i TPdfFutureResult<T>, en record der fortæller svaret, hvilken af tre ting der skete. IsSuccess betyder, at arbejderen returnerede normalt, og Value indeholder gengivelsen. IsCancelled betyder, at tokenet blev udløst, og arbejderen bakkede ud ved et annulleringspunkt. IsFailure betyder, at arbejderen udløste en undtagelse, og ErrorMessage bærer teksten. Svaret inspicerer statussen én gang og forgrener sig i stedet for at gætte ud fra en sentinel-værdi, om et returneret bitmap er ægte
V1.61.0-kapløbet, der ændrede svarlevering
Den mest lærerige del af denne enhed er en en-linjes ændring, det tog et stykke tid at forstå. I tidlige versioner leverede arbejdertråden sit svar med TThread.Queue. Queue poster svaret til hovedtrådens kø og returnerer øjeblikkeligt, hvilket lyder som præcis, hvad en affyr-og-glem-future ønsker. Det var forkert, og grunden er værd at stave ud, fordi det er den slags fejl, der passerer enhver test, man kan finde på at skrive
Arbejdertråden oprettes med FreeOnTerminate := True. Det betyder, at i det øjeblik Execute returnerer, river tråden sig selv ned, og TThread.Destroy kalder RemoveQueuedEvents(Self) som en del af oprydningen. RemoveQueuedEvents sletter enhver i-kø-sat metode, hvis mål er den døende tråd. Så sekvensen var: arbejderen bliver færdig, den sætter svaret i kø mod sig selv, Execute returnerer, tråden ødelægger sig selv, og RemoveQueuedEvents sletter det svar, som hovedtråden endnu ikke havde kørt. Resultatet forsvandt simpelthen. Endnu værre var, at i det snævre vindue, hvor hovedtråden trak det i-kø-satte svar af og begyndte at køre det i samme øjeblik, tråden blev frigivet, rørte svaret ved felter i et halvt-ødelagt objekt, hvilket er en brug-efter-frigivelse-fejl
Løsningen i v1.61.0 var at levere svaret med Synchronize i stedet for Queue. Synchronize blokerer arbejdertråden, indtil hovedtråden har kørt svaret til ende. Arbejderen er stadig i live, mens dens svar udføres, så der er intet at frigive under den, og tråden returnerer ikke fra Execute (og begynder derfor ikke at ødelægge sig selv), før svaret er blevet leveret. Levering er garanteret, og brug-efter-frigivelse-vinduet er lukket
Den generelle lære overlever den specifikke rettelse. Affyr-og-glem asynkrone callbacks er det sværeste samtidighedsmønster at få subtilt forkert, fordi det lykkelige spor fungerer i første forsøg, og fejlen lever i samspillet mellem trådens nedrivningsrækkefølge og køen. Den reproduceres ikke på kommando. Det afhænger af, om hovedtråden tilfældigvis tømte køen, før arbejderen tilfældigvis var færdig med at ødelægge sig selv, hvilket er en timing, som planlæggeren afgør forskelligt ved hver kørsel. En primitiv, der er korrekt én gang, i bindingen, er langt mere værd end den samme kode, der genudledes i enhver applikation, som har brug for en baggrundsgengivelse
Hvorfor callbacks er metode-pointere
Arbejderen og svaret er ikke anonyme metoder. De er procedure of object-typer, TPdfFutureWorker<T> og TPdfFutureReply<T>, og det valg er tvunget frem af kompilermatricen. PDFiumPas kompilerer på Delphi XE5 og nyere samt på Free Pascal 3.2 i Delphi-tilstand, og FPC 3.2 i den tilstand understøtter ikke anonyme metoder. Et reference-til-procedure-callback, der fanger lokale variabler, ville kompilere på Delphi og fejle på FPC, så enheden bruger den laveste fællesnævner, som begge compilere accepterer
Den praktiske konsekvens er, hvor tilstanden lever. En anonym metode lukker over lokale variabler; det gør en metode-pointer ikke. Så enhver tilstand, arbejderen har brug for, sideindekset, zoomet, outputstien, og enhver tilstand, svaret har brug for at opdatere, målbilled-kontrollen eller fremdriftsetiketten, skal hænge på det objekt, hvis metode overdrages. I en fremviser er det objekt normalt formen eller en gengivelsescontroller, den ejer. Dette er ikke en workaround, der er pålagt modvilligt; den holder ejerskabet af den tilstand eksplicit og synligt på det modtagende objekt i stedet for skjult inde i en lukning
Samarbejdende annullering, ikke et hårdt drab
Annullering her er samarbejdende. Der er ingen API, der rækker ind i arbejdertråden og afslutter den, fordi afslutning af en tråd midt i en gengivelse efterlader PDFium med låse og delvist skrevne bitmaps, og procestilstanden efter et tvunget drab er ikke noget, man kan ræsonnere om. I stedet får arbejderen udleveret et skrivebeskyttet token og forventes at tjekke det, og gengivelsessløjfen er skrevet til at tjekke det mellem sider eller mellem fliser, hvor standsning er ren
Tokenet tilbyder tre måder at observere annullering på. IsCancelled er et billigt boolesk tjek til en sløjfe, der ønsker at teste og bestemme selv. ThrowIfCancelled er det almindelige tilfælde: kald den ved et naturligt annulleringspunkt, og hvis der er anmodet om annullering, udløser den EPdfOperationCancelled, hvilket ruller arbejderen direkte tilbage til futuren. RegisterCallback tilknytter en one-shot notifikation, der affyres én gang, når kilden annulleres, nyttigt når en arbejder er blokeret i noget, den kan afbryde frem for at sidde i en stram sløjfe
Undtagelsen er, hvor trådgrænsen betyder noget. Når arbejderen udløser EPdfOperationCancelled, fanger futuren den og forvandler den til en annulleret status, så svaret ser IsCancelled og ikke en fejl. Selve undtagelsesobjektet marshalleres aldrig til hovedtråden. Det lever og dør på arbejdertråden; kun dets meddelelsesstreng kopieres ind i ErrorMessage. At marshallere et levende undtagelsesobjekt på tværs af tråde ville betyde at række ind i hukommelse ejet af en tråd, der er ved at afslutte, hvilket er den samme klasse af fejl, som Synchronize-rettelsen eksisterer for at forhindre. En statuskode og en streng krydser grænsen rent; et objekt ville ikke
To grænseflader, så en arbejder ikke kan annullere sig selv
Annullering er delt over to grænseflader med vilje. IPdfCancellationTokenSource er skrivesiden: den har Cancel, og ejeren, der opretter den, normalt formen, beholder den og kalder Cancel, når brugeren klikker på knappen, eller formen lukkes. IPdfCancellationToken er læsesiden: den har IsCancelled, ThrowIfCancelled og RegisterCallback, og det er alt, hvad arbejderen nogensinde modtager. Ét konkret objekt implementerer begge, men arbejderen får kun nogensinde udleveret tokenet, så den har ingen måde at annullere den operation, den kører. Opdelingen er en sikkerhedsbarriere på API-niveau. En arbejder, der kunne nå Cancel gennem sit token, ville invitere et forvirret stykke kode til at annullere sig selv, og typesystemet fjerner den mulighed
Der er en matchende detalje for det tilfælde, hvor en kalder ønsker en gengivelse, men aldrig har til hensigt at annullere den. I stedet for at gennemtvinge en frisk kilde per kald eksponerer enheden PdfNoCancellationToken, et singleton-token, der permanent befinder sig i den ikke-annullerede tilstand. Run erstatter det, når token-argumentet efterlades som nil. Denne singleton konstrueres ivrigt under enhedsinitialisering frem for dovent ved første brug, og grunden er atter samtidighed. Hvis flere Run-kald på forskellige arbejdertråde alle rakte ud efter en dovent oprettet singleton på én gang, kunne de køre om kap på dens konstruktion, lække en dublet eller kortvarigt observere en halvt-initialiseret instans. At bygge den, før nogen arbejder overhovedet kan køre, fjerner kapløbet fuldstændigt
Kørsel af en annullerbar gengivelse
I praksis opretter du en kilde, beholder den på formen, overdrager dens Token ind i Run sammen med en arbejdermetode og en svarmetode, og forbinder knappen Annuller til kilden. Arbejderen tjekker tokenet, mens den gengiver; svaret opdaterer UI'en, når resultatet er tilbage. Fordi callbacks er metode-pointere, læser arbejderen og svaret, hvad end de har brug for fra formens felter
Svaret håndterer alle tre udfald, fordi alle tre er opnåelige. En færdig gengivelse rapporterer succes, en bruger, der trykkede på Annuller, ser den annullerede gren, og en fil, der ikke kunne skrives, eller en side, mislykkedes at parse, ankommer som en fejl med en meddelelse. Ingen af disse grene blokerer, ingen af dem rører arbejdertråden, og det bitmap eller den status, arbejderen producerede, læses først, efter at futuren har leveret det på den tråd, der ejer UI'en
Den samme tråddisciplin betaler sig andre steder i en fremviser. Den måde, gengivne bitmaps gemmes og genbruges på tværs af zoomændringer, er dækket i vores note om gengivelsescachen og zoomydeevne, og det bredere spørgsmål om at holde PDFium-grænsen sikker under Delphi findes i hærdning af PDFium VCL ABI for hukommelsessikkerhed. Den asynkrone infrastruktur beskrevet her leveres som del af PDFium Component til Delphi og C++Builder, ved siden af de gengivelses-, tekst- og formular-API'er, der er dækket andetsteds på denne blog