Technical Article

การเรนเดอร์ PDF พื้นหลังใน Delphi ด้วย Futures ที่ยกเลิกได้

การเรนเดอร์หน้ากระดาษใน PDFium นั้นเป็นแบบซิงโครนัส (synchronous) คุณเรียกเข้าไปในไลบรารี มันทำแรสเตอร์ไลซ์ (rasterises) ลงในบิตแมปที่คุณมอบให้มัน และการควบคุมก็จะกลับมาเมื่อพิกเซลถูกเขียนลงไป สำหรับหน้ากระดาษขนาดหน้าจอเดียวที่ระดับการซูมเดียว นั่นใช้เวลาไม่กี่มิลลิวินาทีและไม่มีใครสังเกตเห็นได้ สำหรับการส่งออกเอกสาร 200 หน้าที่ความละเอียด 300 dpi หรือแถบภาพขนาดย่อที่ต้องทำแรสเตอร์ไลซ์ทุกหน้าพร้อมกัน การเรียกใช้แบบเดียวกันนั้นใช้เวลาเป็นวินาที หากคุณทำการเรียกใช้นั้นจากเธรดหลัก (main thread) ลูปข้อความ (message loop) จะหยุดทำงาน หน้าต่างหยุดวาดใหม่ และ Windows จะแสดงข้อความ "Not Responding" ที่น่าสะพรึงกลัวบนแถบชื่อเรื่องของคุณ งานนั้นถูกต้องแล้ว แต่สถานที่ที่คุณรันมันนั้นผิด

การแก้ไขคือการย้ายการเรนเดอร์ที่ยาวนานไปยังเธรดพื้นหลัง (background thread) และนำผลลัพธ์กลับมายังเธรดหลัก ซึ่งสามารถส่งมอบบิตแมปให้กับคอนโทรล (control) ได้ PDFium เองไม่ได้ห้ามคุณจากการทำเช่นนี้ แต่ไบน์ดิง (binding) ต้องทำให้การส่งมอบนี้ปลอดภัย เนื่องจากพื้นผิวของบั๊กที่อยู่รอบๆ "รันบนเวิร์กเกอร์ (worker) ตอบกลับบน UI" นั้นกว้างและมีความล้มเหลวเกิดขึ้นเป็นช่วงๆ ยูนิต FPdfAsync ใน PDFiumPas มีอยู่เพื่อให้รูปแบบนั้นมีการปรับใช้งานที่ถูกต้องแบบหนึ่ง พร้อมด้วยโมเดลการยกเลิกที่เข้ากับพฤติกรรมของการเรนเดอร์ที่ยาวนานอย่างแท้จริง

รูปแบบของงาน

การดำเนินการสามอย่างครอบงำกรณีที่การเรนเดอร์อยู่ได้นานกว่าหนึ่งเฟรม การเรนเดอร์แบบกลุ่ม (Batch rendering) จะไล่ตามช่วงหน้าและทำแรสเตอร์ไลซ์แต่ละหน้า โดยปกติจะลงดิสก์ การส่งออกแบบหลายหน้า (Multi-page export) ทำเช่นเดียวกัน แต่ประกอบผลลัพธ์ลงในไฟล์เดียว การเรนเดอร์หน้าพื้นหลัง (Background page rendering) คือสิ่งที่โปรแกรมดูเอกสาร (viewer) ทำเมื่อผู้ใช้ข้ามไปยังหน้าที่ยังไม่ได้อยู่ในแคช ดังนั้นบิตแมปจึงถูกสร้างขึ้นนอกเธรดและแสดงขึ้นเมื่อมันพร้อม ทั้งสามกรณีนี้มีข้อจำกัดเหมือนกัน คือรันนานเกินกว่าที่เธรด UI จะโฮสต์ไว้ได้ พวกมันผลิตผลลัพธ์ที่เธรด UI ต้องการในท้ายที่สุด และผู้ใช้อาจละทิ้งงานเหล่านี้ การปิดเอกสาร การเลื่อนผ่านหน้าไป หรือการกด Cancel ควรจะหยุดการทำงาน แทนที่จะบังคับให้ผู้ใช้รอเอาต์พุตที่พวกเขาไม่ต้องการอีกต่อไป

ข้อจำกัดข้อสุดท้ายนั่นแหละคือสิ่งที่กำหนดรูปแบบการออกแบบ การเรนเดอร์ที่ไม่สามารถยกเลิกได้คือการเรนเดอร์ที่เปิดเอกสารค้างไว้และเผาผลาญ CPU หลังจากคำตอบนั้นหมดความหมายไปแล้ว ดังนั้นยูนิตจึงถูกสร้างขึ้นรอบๆ ค่าดั้งเดิม (primitives) สองอย่างที่ประกอบกัน ได้แก่ ฟิวเจอร์ (future) ที่นำผลลัพธ์กลับมา และโทเค็น (token) ที่ส่งคำขอยกเลิกไปข้างหน้า

ฟิวเจอร์แบบยิงแล้วลืม

TPdfFuture<T>.Run ใช้ตัวเวิร์กเกอร์ ตัวตอบกลับ (reply) และโทเค็นการยกเลิกที่เป็นตัวเลือกเสริม มันเริ่มรันเวิร์กเกอร์บนเธรดพื้นหลัง และเมื่อเวิร์กเกอร์ทำงานเสร็จ มันก็จะส่งมอบการตอบกลับบนเธรดหลัก พารามิเตอร์ประเภทเจเนอริก T คืออะไรก็ตามที่การเรนเดอร์ผลิตขึ้นมา มักจะเป็นแฮนเดิลบิตแมป (bitmap handle) หรือบันทึกสถานะ (status record) เวิร์กเกอร์จะรันนอกเธรด การตอบกลับจะรันในจุดที่ปลอดภัยในการสัมผัส VCL

class procedure TPdfFuture<T>.Run(
  const AWorker: TPdfFutureWorker<T>;
  const AReply: TPdfFutureReply<T>;
  const AToken: IPdfCancellationToken = nil); static;

การละเว้นอย่างจงใจคือ Wait ในรูปแบบใดๆ ก็ตาม ไม่มีเมธอดที่จะบล็อกผู้เรียก (caller) จนกว่าฟิวเจอร์จะเสร็จสมบูรณ์ และนั่นไม่ใช่ความผิดพลาด Wait ที่เรียกจากเธรดหลักคือวิธีคลาสสิกในการทำให้เกิดเดดล็อก (deadlock) ของ UI: เวิร์กเกอร์ต้องการเธรดหลักเพื่อรันการตอบกลับของมันผ่าน Synchronize ในขณะที่เธรดหลักกำลังจอดรออยู่ภายใน Wait และทั้งสองฝ่ายก็ไม่สามารถดำเนินการต่อไปได้ ด้วยการปฏิเสธที่จะเสนอค่าดั้งเดิม (primitive) นี้ ฟิวเจอร์ได้ตัดรูปแบบที่มักจะเอาชนะคนที่พยายามเขียนสิ่งนี้ด้วยตัวเองออกไป โค้ดที่จำเป็นต้องบล็อกจริงๆ ควรใช้ TThread แบบธรรมดาและยอมรับผลที่ตามมาเอง ฟิวเจอร์นั้นมีไว้สำหรับกรณีแบบยิงแล้วลืม ซึ่งก็คือสิ่งที่การเรนเดอร์พื้นหลังเป็นจริงๆ

ผลลัพธ์จะถูกห่อหุ้มอยู่ใน TPdfFutureResult<T> ซึ่งเป็นบันทึกที่บอกการตอบกลับว่าสิ่งใดในสามสิ่งนี้ได้เกิดขึ้น IsSuccess หมายความว่าเวิร์กเกอร์ส่งคืนตามปกติและ Value จะเก็บผลการเรนเดอร์ไว้ IsCancelled หมายความว่าโทเค็นทำงานและเวิร์กเกอร์ได้ถอนตัวออก ณ จุดยกเลิก IsFailure หมายความว่าเวิร์กเกอร์ทำให้เกิดข้อผิดพลาด และ ErrorMessage จะนำข้อความนั้นมาด้วย การตอบกลับจะตรวจสอบสถานะหนึ่งครั้งแล้วแยกสาขา แทนที่จะเดาจากค่าเซนติเนล (sentinel value) ว่าบิตแมปที่ส่งคืนมานั้นเป็นของจริงหรือไม่

การแข่งขัน (race) ในเวอร์ชัน v1.61.0 ที่เปลี่ยนการส่งมอบการตอบกลับ

ส่วนที่เป็นประโยชน์ที่สุดในการเรียนรู้ของยูนิตนี้คือการเปลี่ยนแปลงเพียงบรรทัดเดียวที่ต้องใช้เวลาทำความเข้าใจสักพัก ผ่านเวอร์ชันแรกๆ เธรดเวิร์กเกอร์ส่งมอบการตอบกลับด้วย TThread.Queue Queue จะโพสต์การตอบกลับไปยังคิวของเธรดหลักและส่งคืนทันที ซึ่งดูเหมือนกับสิ่งที่ฟิวเจอร์แบบยิงแล้วลืมต้องการอย่างพอดิบพอดี แต่มันผิด และเหตุผลนั้นก็คุ้มค่าที่จะอธิบายออกมา เพราะมันเป็นชนิดของบั๊กที่สามารถผ่านการทดสอบทุกอย่างที่คุณคิดจะเขียนได้

เธรดของเวิร์กเกอร์ถูกสร้างขึ้นด้วย FreeOnTerminate := True นั่นหมายความว่าในพริบตาที่ Execute ส่งคืน เธรดก็จะรื้อถอนตัวเองลง และ TThread.Destroy จะเรียก RemoveQueuedEvents(Self) เป็นส่วนหนึ่งของการล้างข้อมูล RemoveQueuedEvents จะล้างเมธอดในคิวใดๆ ที่เป้าหมายคือเธรดที่กำลังจะตาย ดังนั้นลำดับขั้นก็คือ: เวิร์กเกอร์ทำงานเสร็จ มันเข้าคิวการตอบกลับเข้าหาตัวมันเอง Execute ส่งคืน เธรดทำลายตัวเอง และ RemoveQueuedEvents จะลบการตอบกลับที่เธรดหลักยังไม่ได้รัน ผลลัพธ์จึงอันตรธานหายไปเฉยๆ ที่แย่กว่านั้น ในช่องว่างแคบๆ ที่เธรดหลักดึงการตอบกลับในคิวออกไปและเริ่มรันมันในวินาทีเดียวกับที่เธรดกำลังถูกปล่อย การตอบกลับก็ไปสัมผัสกับฟิลด์ของออบเจ็กต์ที่ถูกทำลายไปแล้วครึ่งหนึ่ง ซึ่งนี่ก็คือการใช้งานหลังจากหน่วยความจำถูกปล่อยเป็นอิสระ (use-after-free)

การแก้ไขใน v1.61.0 คือการส่งมอบการตอบกลับด้วย Synchronize แทนที่จะเป็น Queue Synchronize จะบล็อกเธรดของเวิร์กเกอร์จนกว่าเธรดหลักจะรันการตอบกลับจนเสร็จสมบูรณ์ เวิร์กเกอร์ยังคงมีชีวิตอยู่ขณะที่การตอบกลับทำงาน ดังนั้นจึงไม่มีอะไรที่จะถูกปลดปล่อยให้เป็นอิสระไปจากใต้เท้าของมัน และเธรดจะไม่ส่งคืนจาก Execute (และด้วยเหตุนี้จึงไม่เริ่มทำลายตัวเอง) จนกว่าจะส่งมอบการตอบกลับเสร็จสิ้น การส่งมอบได้รับการรับประกัน และหน้าต่างของการเกิด use-after-free จะถูกปิดลง

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;

บทเรียนโดยทั่วไปนั้นยืนยาวกว่าการแก้ไขปัญหาเฉพาะจุด โคลแบ็กแบบอะซิงโครนัสชนิดยิงแล้วลืม (fire-and-forget) คือรูปแบบการทำงานพร้อมกัน (concurrency pattern) ที่ง่ายที่สุดที่จะเกิดข้อผิดพลาดได้อย่างแยบยล เพราะเส้นทางแห่งความสุข (happy path) นั้นจะทำงานได้ตั้งแต่ความพยายามครั้งแรก และบั๊กก็อาศัยอยู่ในปฏิสัมพันธ์ระหว่างลำดับการรื้อถอนเธรดกับคิว มันไม่ได้เกิดขึ้นซ้ำๆ ตามต้องการ มันขึ้นอยู่กับว่าเธรดหลักบังเอิญระบายคิวหมดก่อนที่เวิร์กเกอร์จะทำลายตัวเองเสร็จหรือไม่ ซึ่งเป็นช่วงเวลาที่ตัวจัดกำหนดการ (scheduler) ตัดสินใจแตกต่างกันไปในการรันทุกครั้ง ค่าดั้งเดิม (primitive) ที่ถูกต้องเพียงครั้งเดียวในไบน์ดิง จึงมีค่ามากกว่าโค้ดเดียวกันที่ถูกนำไปดัดแปลงและเขียนซ้ำในแอปพลิเคชันทุกตัวที่ต้องการเรนเดอร์พื้นหลัง

ทำไมโคลแบ็กจึงเป็นพอยน์เตอร์เมธอด (method pointers)

เวิร์กเกอร์และการตอบกลับไม่ใช่เมธอดแบบไม่ระบุตัวตน (anonymous methods) พวกมันเป็นประเภท procedure of object คือ TPdfFutureWorker<T> และ TPdfFutureReply<T> และทางเลือกนั้นถูกบังคับโดยตารางคอมไพเลอร์ PDFiumPas ทำการคอมไพล์บน Delphi XE5 ขึ้นไปและบน Free Pascal 3.2 ในโหมด Delphi และ FPC 3.2 ในโหมดนั้นไม่รองรับ anonymous methods การโคลแบ็กอ้างอิงถึงกระบวนการ (reference-to-procedure) ที่จับตัวแปรในตัว (local variables) จะคอมไพล์ผ่านบน Delphi แต่ล้มเหลวบน FPC ดังนั้นยูนิตนี้จึงใช้ตัวหารร่วมน้อยที่สุดที่คอมไพเลอร์ทั้งสองตัวยอมรับ

ผลลัพธ์ในทางปฏิบัติคือตำแหน่งที่จัดเก็บสถานะ anonymous method จะปิดตัวลงเหนือตัวแปรในตัว (locals); พอยน์เตอร์เมธอด (method pointer) ไม่ได้เป็นเช่นนั้น ดังนั้นสถานะใดๆ ที่เวิร์กเกอร์ต้องการ ดัชนีหน้า การซูม เส้นทางเอาต์พุต และสถานะใดๆ ที่การตอบกลับจำเป็นต้องอัปเดต เช่น คอนโทรลอิมเมจเป้าหมายหรือป้ายกำกับความคืบหน้า (progress label) จะต้องห้อยอยู่กับออบเจ็กต์ที่ถูกส่งผ่านเมธอดนั้น ในโปรแกรมดูเอกสาร (viewer) ออบเจ็กต์นั้นมักจะเป็นฟอร์ม (form) หรือตัวควบคุมการเรนเดอร์ (render controller) ที่มันเป็นเจ้าของ สิ่งนี้ไม่ใช่การแก้ปัญหาเฉพาะหน้าที่ถูกยัดเยียดอย่างไม่เต็มใจ แต่มันช่วยรักษาความเป็นเจ้าของสถานะนั้นให้มีความชัดเจนและสามารถมองเห็นได้บนออบเจ็กต์ผู้รับ แทนที่จะถูกซ่อนอยู่ภายในโคลสเจอร์ (closure)

การยกเลิกแบบร่วมมือ (Cooperative cancellation) ไม่ใช่การฆ่าอย่างกะทันหัน (hard kill)

การยกเลิกที่นี่เป็นแบบร่วมมือกัน ไม่มี API ใดที่เอื้อมมือเข้าไปในเธรดเวิร์กเกอร์แล้วยุติมันทิ้ง เพราะการยุติเธรดในระหว่างการเรนเดอร์จะทำให้ PDFium จับถือล็อก (locks) และบิตแมปที่ถูกเขียนไปแล้วเพียงบางส่วนเอาไว้ และสถานะของกระบวนการหลังจากถูกบังคับฆ่านั้นไม่ใช่สิ่งที่คุณจะให้เหตุผลได้ ในทางตรงกันข้าม เวิร์กเกอร์จะได้รับโทเค็นแบบอ่านอย่างเดียวและถูกคาดหวังให้ตรวจสอบมัน และลูปการเรนเดอร์ก็ถูกเขียนขึ้นเพื่อตรวจสอบมันระหว่างหน้าต่างๆ หรือระหว่างช่องสี่เหลี่ยม (tiles) ซึ่งจะเป็นจุดที่การหยุดทำงานนั้นสะอาดหมดจด

โทเค็นเสนอวิธีสามทางในการสังเกตการยกเลิก IsCancelled เป็นการโพล (poll) บูลีนราคาถูกสำหรับลูปที่ต้องการทดสอบและตัดสินใจด้วยตัวเอง ThrowIfCancelled คือกรณีทั่วไป: เรียกมันที่จุดยกเลิกที่เป็นธรรมชาติและหากมีการขอยกเลิก มันจะทำให้เกิด EPdfOperationCancelled ซึ่งจะคลี่เวิร์กเกอร์ให้ย้อนกลับไปยังฟิวเจอร์โดยตรง RegisterCallback จะแนบการแจ้งเตือนแบบครั้งเดียว (one-shot notification) ที่จะยิงออกมาหนึ่งครั้งเมื่อต้นทางถูกยกเลิก ซึ่งมีประโยชน์เมื่อเวิร์กเกอร์ถูกบล็อกอยู่ในสิ่งที่มันสามารถขัดจังหวะได้ แทนที่จะนั่งอยู่ในลูปที่แน่นขนัด

ข้อยกเว้น (exception) คือที่ที่ขอบเขตของเธรดมีความสำคัญ เมื่อเวิร์กเกอร์ทำให้เกิด EPdfOperationCancelled ฟิวเจอร์จะรับมันไว้และเปลี่ยนเป็นสถานะถูกยกเลิก (cancelled status) ดังนั้นการตอบกลับจึงมองเห็น IsCancelled และไม่ใช่ความล้มเหลว (failure) ออบเจ็กต์ข้อยกเว้นตัวมันเองจะไม่เคยถูกเรียบเรียง (marshaled) ข้ามไปยังเธรดหลัก มันจะเกิดและตายบนเธรดเวิร์กเกอร์ มีเพียงสตริงข้อความของมันเท่านั้นที่ถูกคัดลอกเข้าสู่ ErrorMessage การเรียบเรียงออบเจ็กต์ข้อยกเว้นที่ยังมีชีวิตข้ามเธรดต่างๆ จะหมายถึงการเอื้อมเข้าไปในหน่วยความจำที่เธรดที่กำลังจะเสร็จสิ้นการทำงานเป็นเจ้าของ ซึ่งถือเป็นคลาสของความผิดพลาดแบบเดียวกันกับที่การแก้ไข Synchronize มีไว้เพื่อป้องกัน รหัสสถานะและสตริงข้ามขอบเขตไปได้อย่างหมดจด แต่ออบเจ็กต์นั้นทำไม่ได้

สองอินเทอร์เฟซ เพื่อไม่ให้เวิร์กเกอร์สามารถยกเลิกตัวเองได้

การยกเลิกถูกแบ่งข้ามสองอินเทอร์เฟซด้วยความตั้งใจ IPdfCancellationTokenSource คือฝั่งเขียน: มันมี Cancel และเจ้าของที่สร้างมันขึ้นมา (มักจะเป็นฟอร์ม) จะเก็บมันไว้และเรียก Cancel เมื่อผู้ใช้คลิกปุ่มหรือฟอร์มปิดลง IPdfCancellationToken คือฝั่งอ่าน: มันมี IsCancelled, ThrowIfCancelled และ RegisterCallback และนั่นคือทั้งหมดที่เวิร์กเกอร์จะเคยได้รับ ออบเจ็กต์รูปธรรมหนึ่งตัวช่วยอิมพลีเมนต์ทั้งสองอย่าง แต่เวิร์กเกอร์จะได้รับเพียงแค่โทเค็นเท่านั้น ดังนั้นมันจึงไม่มีวิธีที่จะยกเลิกการทำงานที่มันกำลังรันอยู่ได้ การแบ่งแยกนี้คือราวกั้นระดับ API เวิร์กเกอร์ที่สามารถเข้าถึง Cancel ผ่านโทเค็นของมัน จะเป็นตัวเชื้อเชิญให้โค้ดที่สับสนชิ้นหนึ่งยกเลิกตัวเอง และระบบประเภทข้อมูล (type system) ก็จะกำจัดความเป็นไปได้นั้นออกไป

มีรายละเอียดที่ตรงกันสำหรับกรณีที่ผู้เรียกต้องการเรนเดอร์แต่ไม่เคยตั้งใจที่จะยกเลิกมันเลย แทนที่จะบังคับให้มีต้นทาง (source) ใหม่ต่อการเรียกทุกครั้ง ยูนิตได้เผย PdfNoCancellationToken ซึ่งเป็นโทเค็นซิงเกิลตัน (singleton) ที่อยู่ในสถานะไม่ถูกยกเลิกแบบถาวร Run จะเข้าแทนที่มันเมื่อปล่อยอาร์กิวเมนต์โทเค็นไว้เป็นค่าว่าง (nil) ซิงเกิลตันนั้นถูกสร้างขึ้นอย่างกระตือรือร้นในระหว่างการเตรียมยูนิตให้พร้อมทำงาน แทนที่จะถูกสร้างแบบขี้เกียจในการใช้งานครั้งแรก และเหตุผลก็คือเรื่องการทำงานพร้อมกัน (concurrency) อีกครั้ง หากการเรียก Run หลายครั้งบนเธรดเวิร์กเกอร์ที่แตกต่างกันพยายามเข้าถึงซิงเกิลตันที่ถูกสร้างแบบขี้เกียจพร้อมๆ กัน พวกมันอาจจะแข่งขันกัน (race) ในการสร้างมันขึ้นมา ทำซ้ำแบบรั่วไหล หรืออาจจะมองเห็นตัวอย่างที่พึ่งจะพร้อมใช้งานไปครึ่งๆ กลางๆ (half-initialised instance) ได้ในชั่วครู่ การสร้างมันก่อนที่เวิร์กเกอร์ใดๆ จะสามารถรันได้ ถือเป็นการกำจัดการแข่งขันออกไปโดยสิ้นเชิง

การรันเรนเดอร์ที่ยกเลิกได้

ในทางปฏิบัติ คุณสร้างต้นทาง (source) เก็บมันไว้บนฟอร์ม ส่งผ่าน Token ของมันเข้าไปใน Run ควบคู่ไปกับเมธอดเวิร์กเกอร์และเมธอดการตอบกลับ และเชื่อมโยงปุ่ม Cancel เข้ากับต้นทาง เวิร์กเกอร์จะตรวจสอบโทเค็นในขณะที่ทำการเรนเดอร์; การตอบกลับจะอัปเดต UI เมื่อผลลัพธ์กลับมา เนื่องจากโคลแบ็กคือพอยน์เตอร์เมธอด (method pointers) เวิร์กเกอร์และการตอบกลับจึงจะอ่านอะไรก็ตามที่พวกมันต้องการจากฟิลด์ต่างๆ ของฟอร์ม

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;

การตอบกลับจะจัดการกับผลลัพธ์ทั้งสามแบบเพราะทั้งสามนั้นสามารถเข้าถึงได้ การเรนเดอร์ที่เสร็จสมบูรณ์จะรายงานความสำเร็จ (success) ผู้ใช้ที่กด Cancel จะเห็นสาขาที่ถูกยกเลิก (cancelled) และไฟล์ที่ไม่สามารถเขียนลงไปได้หรือหน้าที่ล้มเหลวในการแยกวิเคราะห์ (parse) จะมาถึงในฐานะความล้มเหลว (failure) พร้อมกับข้อความ ไม่มีสาขาใดที่จะบล็อกการทำงาน ไม่มีสาขาใดที่ไปสัมผัสกับเธรดเวิร์กเกอร์ และบิตแมปหรือสถานะที่เวิร์กเกอร์ผลิตขึ้นจะถูกอ่านหลังจากที่ฟิวเจอร์ได้ทำการส่งมอบมันบนเธรดที่เป็นเจ้าของ UI แล้วเท่านั้น

ระเบียบวินัยเกี่ยวกับเธรดแบบเดียวกันนี้ยังคุ้มค่าในจุดอื่นๆ ของโปรแกรมดูเอกสาร (viewer) ด้วย วิธีที่บิตแมปที่เรนเดอร์แล้วจะถูกเก็บรักษาและนำกลับมาใช้ใหม่ผ่านการเปลี่ยนแปลงการซูม จะมีครอบคลุมอยู่ใน บันทึกของเราเกี่ยวกับแคชการเรนเดอร์และประสิทธิภาพของการซูม และคำถามที่กว้างกว่าเกี่ยวกับการรักษาขอบเขตของ PDFium ให้ปลอดภัยภายใต้ Delphi มีอยู่ใน การทำให้ ABI ของ VCL ใน PDFium แข็งแกร่งเพื่อความปลอดภัยของหน่วยความจำ โครงสร้างพื้นฐานแบบอะซิงโครนัสที่อธิบายไว้ที่นี่มีจัดส่งให้โดยเป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder ควบคู่ไปกับ API เพื่อการเรนเดอร์ ข้อความ และฟอร์ม ที่ครอบคลุมในส่วนอื่นๆ ของบล็อกนี้