Paketinis atvaizdavimas užsišaldo pusiaukelėje, nes vykdytuvas PDFium Component nelaiko užduoties baigta, kol jos atsakymas nebuvo išsiųstas. Su padSynchronize tas atsakymas vyksta pagrindinėje gijoje. Jei pagrindinė gija blokuojasi, neleisdama CheckSynchronize, darbinė gija laukia pagrindinės gijos, kol pagrindinė gija laukia laisvos būsenos
Derintuvo vaizdas nesuklystamas, kai jį pamatai. Pristabdykite užšalusį procesą, ir pagrindinė gija sėdi laukime laisvos būsenos įvykio, keliais kadrais žemiau jūsų pačios paketinio ciklo. Perjunkite į bet kurią darbinę giją, ir ji sėdi viduje TThread.Synchronize, laikydama baigtą rezultatą, kurio negali perduoti. Niekas nesisuka, joks CPU nedega, procesas tiesiog stovi. Šis straipsnis yra apie tai, kodėl ta būsena apskritai egzistuoja, ir apie tris gretimas taisykles, nulemiančias, ar Delphi darbininkų telkinys virš PDFium elgiasi, ar kandžiojasi: ką iš tikrųjų riboja QueueCapacity, kokia tvarka turi vykti atšaukimas išjungimo metu, ir ko lygiagretumas nesuteikia PDFium objektų nuosavybės atžvilgiu
Kodėl WaitForIdle pakabina pagrindinę giją?
Jis pakabina todėl, kad laisva būsena TPdfAsyncExecutor apibrėžiama kaip apimanti atsakymo išsiuntimą, ne tik darbo užbaigimą. Veikiančių skaičius padidinamas DequeueTask, kai darbininkas paima užduotį, ir sumažinamas TaskFinished, kurį darbininkas kviečia tik po to, kai TPdfAsyncTaskOperation.Execute grįžo. Tas metodas vykdo darbininko kūną, įrašo rezultatą, o tada išsiunčia atsakymą pagal TPdfAsyncDispatchMode. Su padSynchronize siuntimas yra TThread.Synchronize iškvietimas, todėl Execute negrįžta, kol pagrindinė gija jo nepavykdė
Delphi antrą sutarties pusę uždeda ant jūsų. TThread.Synchronize prideda metodą prie globalios eilės ir blokuoja kviečiančią giją įvykiu; kažkas pagrindinėje gijoje turi iškviesti CheckSynchronize, prieš tam įvykiui kada nors signalizuojant. VCL pranešimų ciklas tai daro už jus tarp pranešimų, ir būtent todėl klaida nematoma interaktyvaus naudojimo metu ir pasirodo tą akimirką, kai parašote blokuojantį paketinį ciklą. Blokuojanti pagrindinė gija yra pagrindinė gija, palikusi pranešimų ciklą, o pagrindinė gija už pranešimų ciklo ribų nieko neišvalo
uses
System.Classes, FPdfAsync, PDFium;
// The shape that deadlocks: a synchronized reply plus a blocking main thread
Task := Executor.Submit(RenderPageWorker, PageRendered, papNormal,
padSynchronize);
Task.WaitFor(High(Cardinal)); // the main thread now parks in a kernel wait
// Meanwhile TPdfAsyncTaskOperation.Execute has reached:
// TThread.Synchronize(AWorkerThread, DispatchReply);
// which enqueues DispatchReply and waits for the main thread to drain it.
// The main thread is draining nothing, so both sides wait forever.
Užduoties užbaigimas ir vykdytuvo laisva būsena yra du skirtingi etapai
Jie atskirti sąmoningai, ir žinojimas, kurio iš jų laukiate, yra visas taisymas. IPdfAsyncTask.WaitFor patenkinamas tą akimirką, kai darbininko rezultatas nuspręstas: Complete įrašo galutinę TPdfAsyncTaskState ir nustato baigto įvykį prieš svarstant bet kokį atsakymą. TPdfAsyncExecutor.WaitForIdle patenkinamas vėliau, kai ir eilėje esančių, ir vykdomų skaičiai yra nulis, o vykdomų skaičius nesumažėja, kol atsakymas nepasiekė. Todėl užduotis gali būti patsSucceeded ir stebima per Snapshot, kol vykdytuvas vis dar teisėtai užimtas
// TPdfAsyncExecutor.WaitForIdle already pumps for you: it waits on the idle
// event in short slices and calls CheckSynchronize(0) between them.
if not Executor.WaitForIdle(30000) then
ReportBatchTimeout;
// Any hand-rolled main-thread wait has to do the same thing explicitly.
function WaitForTaskOnMainThread(const ATask: IPdfAsyncTask;
ATimeoutMs: Cardinal): Boolean;
var
StartedAt: UInt64;
begin
StartedAt := PdfAsyncTick;
repeat
if ATask.WaitFor(10) then
Exit(True);
CheckSynchronize(0); // release any pending padSynchronize reply
Result := PdfAsyncTickDelta(StartedAt, PdfAsyncTick) < ATimeoutMs;
until not Result;
end;
Viena pasekmė, verta įsisavinti: atsakymas, kuris kelia klaidą, nepersirašo istorijos. DispatchReply pagauna išimtį ir ją saugo ReplyErrorMessage, palikdamas State, CancellationReason ir ErrorMessage lygiai tokius, kokius nustatė darbininkas. UI atgalinis iškvietimas, sprogstantis piešiant miniatiūrą, todėl niekada nepaverčia sėkmingo atvaizdavimo nepavykusiu, o jūsų telemetrija toliau praneša, ką atvaizdavimo variklis iš tikrųjų padarė. Jei norite atgalinio-iškvietimo formos API vienai operacijai, o ne telkiniui, foninis atvaizdavimas su atšaukiamais ateities objektais apima tą kelią
Ar QueueCapacity riboja ir veikiančius darbininkus?
Ne. QueueCapacity PDFium Component skaičiuoja tik eilėje esančias užduotis, niekada tas, kurios jau vykdomos darbininke. Tai sąmoninga: talpa skirta išreikšti realų atgalinį spaudimą laukimo eilei, o fiksuotų lygiagretumo vietų sulankstymas į tą patį skaičių jas skaičiuotų du kartus. Su keturiais darbininkais ir talpa aštuoni galite turėti dvylika užduočių skrydyje, o GetStats sąžiningai praneša padalinį per QueuedCount ir RunningCount
var
Stats: TPdfAsyncExecutorStats;
Task: IPdfAsyncTask;
begin
// TrySubmit never raises: it returns False when the waiting line is full or
// the executor is already shutting down, and bumps RejectedCount.
if not Executor.TrySubmit(RenderPageWorker, PageRendered, Task, papHigh,
padSynchronize) then
begin
Stats := Executor.GetStats;
// QueuedCount is what QueueCapacity bounds. RunningCount is bounded by
// WorkerCount and is never charged against the capacity.
LogBackpressure(Stats.QueuedCount, Stats.RunningCount,
Stats.RejectedCount);
Exit;
end;
Keturios TPdfAsyncPriority juostos yra griežtos, ne svertinės. DequeueTask eina nuo papCritical žemyn iki papLow ir ima pirmą netuščią juostą, išsaugodama FIFO tvarką kiekvienoje juostoje. Tai suteikia interaktyviam prašymui švarų būdą peršokti dar nepradėtą paketą, tačiau niekada nepertraukia jau vykstančio darbo, o iškvietėjas, nuolat maitinantis papCritical, gali neribotai badauti papLow. Rezervuokite dvi viršutines juostas dalykams, kurių žmogus matomai laukia, ir palikite masinį eksportą papNormal ar žemiau. Naudokite Submit, kai pilna eilė yra programavimo klaida, verta EPdfAsyncQueueFull, ir TrySubmit, kai tai normali sąlyga, kurią ketinate tvarkyti
Kodėl Shutdown atšaukia už vykdytuvo užrakto ribų?
Todėl, kad atšaukimas jo viduje apverstų užrakto tvarką ir pakabintų jūsų vykdomą išjungimą. Shutdown(True) paima vykdytuvo užraktą, perjungia išjungimo vėliavėlę ir prideda kiekvieną laukiančią užduotį prie vietinio nuotraukos masyvo per AppendSnapshot. Tada jis atlaisvina užraktą ir tik po to vaikšto per nuotrauką, kviesdamas Cancel kiekvienam įrašui. Užduoties atšaukimas iškelia vartotojo atgalinius iškvietimus, registruotus jos žetono šaltinyje, o tie atgaliniai iškvietimai yra įprastas programos kodas: jie gali paklausti GetStats, pateikti kompensuojantį darbą ar laukti laisvos būsenos. Kiekvienas iš jų pakartotinai patenka į vykdytuvo užraktą, o atgalinis iškvietimas, iškviestas laikant tą užraktą, sukurtų aklavietę prieš save patį
Žetono šaltinis paklūsta tai pačiai drausmei vienu lygiu žemiau. CancelWithReason paima šaltinio užraktą, nusprendžia vienintelį laimintį atšaukėją, įrašo Reason, CancellationMessage ir CancelledAtTick, ir tik tada atomiškai perjungia atšaukimo vėliavėlę. Publikuoti prieš perjungiant yra tai, kas metaduomenis saugu skaityti: bet kuri gija, stebinti IsCancelled kaip True, garantuotai už jo ras pilną priežastį, o vėlesni iškvietėjai pralaimi lenktynes, grąžina False ir negali perrašyti pirmos priežasties. Registruoti atgaliniai iškvietimai fiksuojami nuotraukoje ir išvalomi užrakto viduje, tačiau iškviečiami už jo ribų, kiekvienas apvyniotas taip, kad viena nepavykusi tvarkyklė negalėtų nuslopinti likusiųjų. Jau vykdomos užduotys niekada nežudomos; jos baigiasi kooperatyviai, kai jų darbininko kūnas kitą kartą iškviečia ThrowIfCancelled, todėl Shutdown baigiasi siurbiančiu WaitForIdle prieš prijungiant gijas
Ar lygiagretūs darbininkai atlaisvina PDFium objektų nuosavybę?
Ne, ir tai riba, kurią lengviausiai neteisingai suprasti. TPdfAsyncExecutor planuoja darbą; jis nieko neteigia apie bet ko, ką liečiate to darbo viduje, gijos priklausomybę. Gyvas TPdf egzempliorius netampa lygiagrečiai pasiekiamas dėl to, kad du darbininkai atsitiktinai į jį iškviečia, o vidinis atvaizdavimo užraktas yra apsauga nuo persidengiančių atvaizdavimo iškvietimų, ne licencija dalintis dokumentu tarp gijų. Lygiagretus atvaizdavimas ar eksportas reiškia vieną TPdf vienam darbininkui, sukurtą ir sunaikintą darbo viduje
type
TPageRenderJob = class
private
FFileName: string;
FPageIndex: Integer;
public
procedure Run(const AToken: IPdfCancellationToken);
end;
procedure TPageRenderJob.Run(const AToken: IPdfCancellationToken);
var
LocalPdf: TPdf; // one document instance per worker, never shared
Bmp: TBitmap;
begin
LocalPdf := TPdf.Create(nil);
try
LocalPdf.FileName := FFileName;
LocalPdf.Active := True;
LocalPdf.PageNumber := FPageIndex;
AToken.ThrowIfCancelled;
Bmp := LocalPdf.RenderPage(0, 0, 1024, 1448);
try
HandOffBitmap(FPageIndex, Bmp); // ownership moves to the reply stage
finally
Bmp.Free;
end;
finally
LocalPdf.Free;
end;
end;
Kaina reali ir verta paminėti: kiekvienas darbininkas apmoka savo pačio nagrinėjimą ir savo pačio puslapio podėlį, todėl atmintis mastelio didėja su darbininkų skaičiumi, o ne dokumentų skaičiumi. Tai yra modelio kaina, kur darbininką galima atšaukti ar sugadinti, nesugadinant nieko kito. Jei jūsų darbininkai vietoj to dalijasi peržiūros-pusės dokumentu, užrakinimo taisyklės aplink tai aprašytos atvaizdavimo užrakte ir iškvietimuose, kurie jo nepasiekia, o vieno dokumento atšaukiamas kelias yra atšaukiamame progresyviame atvaizdavime
Publikuotos sąsajos praplėtimas nesulaužant vtable
IPdfCancellationToken ir IPdfCancellationTokenSource yra COM-stiliaus sąsajos, kurias išoriniai dvejetainiai failai gali jau naudoti, todėl metodo pridėjimas prie bet kurios iš jų pastūmėtų kiekvieną vėlesnę vietą vtable ir tyliai neteisingai maršrutizuotų iškvietimus, kompiliuotus prieš seną išdėstymą. Diagnostikos, laukimo, pašalinamo atgalinio iškvietimo ir atominio-atšaukimo galimybės todėl gyvena IPdfCancellationTokenEx ir IPdfCancellationTokenSourceEx, kurios paveldi, o ne modifikuoja. New ir Run išlaiko savo pradinę semantiką esamiems iškvietėjams; naujas kodas siekia NewEx, NewTimeout ir RunEx, kai nori CancelWithReason, WaitForCancellation ar valdomą IPdfCancellationRegistration. Paveldėjimas yra vienintelis saugus būdas augint publikuotą sąsają, ir jis kainuoja vieną papildomą tipą kiekvienai kartai
NewTimeout nusipelno sąžiningos pastabos. Kiekvienas laiko limito šaltinis turi lengvą giją, laukiančią atšaukimo įvykio ar galutinio termino, kuris pirmas ateis. Saujelei ar keliems dešimtims galutinių terminų tai paprasta, žemos latencijos ir identiška Delphi, Lazarus ir C++Builder aplinkose. Tūkstančiams trumpų galutinių terminų tai neteisinga forma, ir jums reikėtų valdyti atšaukimą iš vieno programos lygio laikmačio, o ne laikyti tūkstančius laukiančių gijų
Nė viena iš šių taisyklių nėra egzotiška, kai parašyta, tačiau kiekviena iš jų yra produkcijos incidentas, kai neparašyta. Lauk teisingo etapo ir leisk kažkam siurbti synchronize eilę, skaityk QueueCapacity tik kaip laukimo eilės ribą, atšaukinėk už savo užraktų ribų ir duok kiekvienam darbininkui savo pačio dokumentą. Čia aprašytas asinchroninis sluoksnis pristatomas kaip Delphi PDFium Component dalis, kartu su atvaizdavimo, teksto ir formos API, kurį jis planuoja